From ff248a1d113daf35f659c9dd1625ad62092848d0 Mon Sep 17 00:00:00 2001 From: Andrew Schlackman <72105194+sei-aschlackman@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:11:53 -0400 Subject: [PATCH 1/2] permissions revamp - moved from pre-defined roles to granular permissions - custom roles can be defined and given specific permissions - added Project level roles - Users can be added to specific Projects and assigned Project roles - added Groups of users that can be added to Projects - Roles and Groups can optionally be pulled from Identity Provider token - new settings for identity provider under ClaimsTransformation - see docs/Permissions.md for details - fixed ports in Dockerfile --- Dockerfile | 6 +- docs/Permissions.md | 127 ++ src/Caster.Api/Data/CasterContext.cs | 7 +- ...220192002_Granular_Permissions.Designer.cs | 1421 +++++++++++++++++ .../20241220192002_Granular_Permissions.cs | 230 +++ ...192404_Migrate_Old_Permissions.Designer.cs | 1421 +++++++++++++++++ .../20241220192404_Migrate_Old_Permissions.cs | 69 + ...0200146_Remove_Old_Permissions.Designer.cs | 1337 ++++++++++++++++ .../20241220200146_Remove_Old_Permissions.cs | 82 + ...41220202800_Unique_Group_Names.Designer.cs | 1340 ++++++++++++++++ .../20241220202800_Unique_Group_Names.cs | 28 + ...102200043_Add_Import_Resources.Designer.cs | 1340 ++++++++++++++++ .../20250102200043_Add_Import_Resources.cs | 33 + .../Migrations/CasterContextModelSnapshot.cs | 341 +++- src/Caster.Api/Domain/Models/Apply.cs | 2 +- src/Caster.Api/Domain/Models/Directory.cs | 2 +- src/Caster.Api/Domain/Models/File.cs | 30 +- src/Caster.Api/Domain/Models/FileVersion.cs | 6 +- src/Caster.Api/Domain/Models/Group.cs | 32 + .../Domain/Models/GroupMembership.cs | 50 + src/Caster.Api/Domain/Models/Host.cs | 2 +- src/Caster.Api/Domain/Models/IEntity.cs | 3 + .../Domain/Models/Modules/Design.cs | 4 +- .../Domain/Models/Modules/Module.cs | 2 +- .../Domain/Models/Modules/ModuleOutput.cs | 11 - .../Domain/Models/Modules/ModuleVersion.cs | 2 +- .../Domain/Models/Modules/Variable.cs | 2 +- src/Caster.Api/Domain/Models/Partition.cs | 2 +- src/Caster.Api/Domain/Models/Permission.cs | 37 - src/Caster.Api/Domain/Models/Plan.cs | 6 +- src/Caster.Api/Domain/Models/Pool.cs | 2 +- src/Caster.Api/Domain/Models/Project.cs | 4 +- .../Domain/Models/ProjectMembership.cs | 67 + src/Caster.Api/Domain/Models/ProjectRole.cs | 80 + .../Domain/Models/RemovedResource.cs | 2 +- src/Caster.Api/Domain/Models/Run.cs | 2 +- src/Caster.Api/Domain/Models/SystemRole.cs | 99 ++ src/Caster.Api/Domain/Models/User.cs | 9 +- .../Domain/Models/UserPermission.cs | 50 - src/Caster.Api/Domain/Models/Vlan.cs | 2 +- src/Caster.Api/Domain/Models/Workspace.cs | 2 +- .../Domain/Services/UserClaimsService.cs | 194 ++- .../Features/Applies/Requests/Execute.cs | 17 +- .../Features/Applies/Requests/Get.cs | 21 +- .../Features/Applies/Requests/GetByRun.cs | 20 +- .../Features/DesignModules/MappingProfile.cs | 1 + .../Requests/AddOrUpdateValues.cs | 26 +- .../Features/DesignModules/Requests/Create.cs | 21 +- .../Features/DesignModules/Requests/Delete.cs | 18 +- .../Features/DesignModules/Requests/Edit.cs | 21 +- .../Features/DesignModules/Requests/Get.cs | 17 +- .../DesignModules/Requests/GetByDesign.cs | 17 +- .../DesignModules/Requests/SetEnabled.cs | 25 +- .../Features/Designs/Requests/Create.cs | 23 +- .../Features/Designs/Requests/Delete.cs | 18 +- .../Features/Designs/Requests/Edit.cs | 21 +- .../Features/Designs/Requests/Get.cs | 18 +- .../Designs/Requests/GetByDirectory.cs | 19 +- .../Features/Designs/Requests/SetEnabled.cs | 19 +- .../Features/Directories/Requests/BaseEdit.cs | 19 +- .../Features/Directories/Requests/Create.cs | 40 +- .../Features/Directories/Requests/Delete.cs | 40 +- .../Features/Directories/Requests/Edit.cs | 25 +- .../Features/Directories/Requests/Export.cs | 37 +- .../Features/Directories/Requests/Get.cs | 37 +- .../Features/Directories/Requests/GetAll.cs | 34 +- .../Directories/Requests/GetByExercise.cs | 96 -- .../Directories/Requests/GetByProject.cs | 77 + .../Directories/Requests/GetChildren.cs | 37 +- .../Features/Directories/Requests/Import.cs | 69 +- .../Directories/Requests/PartialEdit.cs | 25 +- .../Features/Files/Requests/AdminLock.cs | 42 +- .../Features/Files/Requests/AdminUnlock.cs | 42 +- .../Features/Files/Requests/Create.cs | 72 +- .../Features/Files/Requests/Delete.cs | 43 +- .../Features/Files/Requests/Edit.cs | 65 +- .../Files/Requests/FileCommandHandler.cs | 69 +- src/Caster.Api/Features/Files/Requests/Get.cs | 35 +- .../Features/Files/Requests/GetAll.cs | 42 +- .../Features/Files/Requests/GetByDirectory.cs | 55 +- .../Features/Files/Requests/GetFileVersion.cs | 37 +- .../Files/Requests/GetFileVersions.cs | 40 +- .../Features/Files/Requests/Lock.cs | 33 +- .../Features/Files/Requests/PartialEdit.cs | 66 +- .../Features/Files/Requests/Rename.cs | 47 +- src/Caster.Api/Features/Files/Requests/Tag.cs | 57 +- .../Features/Files/Requests/Unlock.cs | 31 +- .../EventHandlers/AuthCacheEventHandler.cs | 40 + .../EventHandlers/SignalREventHandler.cs | 58 + src/Caster.Api/Features/Groups/Group.cs | 20 + .../GroupMembership.cs} | 15 +- .../Features/Groups/GroupsController.cs | 151 ++ .../Features/Groups/MappingProfile.cs | 21 + .../Features/Groups/Requests/Create.cs | 43 + .../Groups/Requests/CreateMembership.cs | 71 + .../Features/Groups/Requests/Delete.cs | 43 + .../Groups/Requests/DeleteMembership.cs | 52 + .../Features/Groups/Requests/Edit.cs | 54 + .../Features/Groups/Requests/Get.cs | 50 + .../Features/Groups/Requests/GetAll.cs | 48 + .../Features/Groups/Requests/GetMembership.cs | 54 + .../Groups/Requests/GetMemberships.cs | 53 + .../Features/Hosts/Requests/Create.cs | 38 +- .../Features/Hosts/Requests/Delete.cs | 33 +- .../Features/Hosts/Requests/Edit.cs | 39 +- src/Caster.Api/Features/Hosts/Requests/Get.cs | 39 +- .../Features/Hosts/Requests/GetAll.cs | 37 +- .../Features/Hosts/Requests/PartialEdit.cs | 32 +- .../Features/Modules/Requests/Create.cs | 44 +- .../Modules/Requests/CreateFromRepository.cs | 44 +- .../Modules/Requests/CreateSnippet.cs | 33 +- .../Features/Modules/Requests/Delete.cs | 37 +- .../Features/Modules/Requests/Get.cs | 41 +- .../Features/Modules/Requests/GetAll.cs | 51 +- .../Features/Modules/Requests/GetVersions.cs | 46 +- .../Features/Permissions/Permission.cs | 21 - .../Permissions/PermissionsController.cs | 83 - .../Features/Permissions/Requests/Get.cs | 70 - .../Features/Permissions/Requests/GetAll.cs | 59 - .../Permissions/Requests/GetByUser.cs | 69 - .../Features/Permissions/Requests/GetMine.cs | 57 - src/Caster.Api/Features/Plans/Requests/Get.cs | 37 +- .../Features/Plans/Requests/GetByRun.cs | 49 +- .../MappingProfile.cs | 5 +- .../ProjectPermissionsController.cs | 39 + .../ProjectPermissions/Requests/GetMine.cs | 35 + .../MappingProfile.cs | 9 +- .../Features/ProjectRoles/ProjectRole.cs | 19 + .../ProjectRoles/ProjectRolesController.cs | 53 + .../Features/ProjectRoles/Requests/Get.cs | 55 + .../Features/ProjectRoles/Requests/GetAll.cs | 49 + .../EventHandlers/AuthCacheEventHandler.cs | 64 + .../EventHandlers/SignalREventHandler.cs | 58 + .../Features/Projects/MappingProfile.cs | 2 + src/Caster.Api/Features/Projects/Project.cs | 2 + .../Features/Projects/ProjectMembership.cs | 32 + .../Features/Projects/ProjectsController.cs | 76 +- .../Features/Projects/Requests/Create.cs | 43 +- .../Projects/Requests/CreateMembership.cs | 78 + .../Features/Projects/Requests/Delete.cs | 35 +- .../Projects/Requests/DeleteMembership.cs | 47 + .../Features/Projects/Requests/Edit.cs | 43 +- .../Projects/Requests/EditMembership.cs | 62 + .../Features/Projects/Requests/Export.cs | 42 +- .../Features/Projects/Requests/Get.cs | 39 +- .../Features/Projects/Requests/GetAll.cs | 55 +- .../Projects/Requests/GetMembership.cs | 54 + .../Projects/Requests/GetMemberships.cs | 53 + .../Features/Projects/Requests/Import.cs | 63 +- .../Resources/Commands/BaseOperation.cs | 73 +- .../Features/Resources/Commands/Import.cs | 28 +- .../Features/Resources/Commands/Output.cs | 33 +- .../Features/Resources/Commands/Refresh.cs | 33 +- .../Features/Resources/Commands/Remove.cs | 26 +- .../Features/Resources/Commands/Taint.cs | 31 +- .../Features/Resources/Queries/Get.cs | 34 +- .../Resources/Queries/GetByWorkspace.cs | 30 +- .../Features/Runs/Requests/Cancel.cs | 48 +- .../Features/Runs/Requests/Create.cs | 82 +- src/Caster.Api/Features/Runs/Requests/Get.cs | 36 +- .../Features/Runs/Requests/GetAll.cs | 38 +- .../Features/Runs/Requests/GetByWorkspace.cs | 57 +- .../Features/Runs/Requests/Reject.cs | 67 +- .../Features/Runs/Requests/SaveState.cs | 64 +- src/Caster.Api/Features/Shared/BaseHandler.cs | 70 +- .../Shared/Services/ValidationService.cs | 30 + .../MappingProfile.cs} | 13 +- .../SystemPermissions/Requests/GetMine.cs | 32 + .../SystemPermissionsController.cs | 38 + .../EventHandlers/AuthCacheEventHandler.cs | 53 + .../EventHandlers/SignalREventHandler.cs | 61 + .../Features/SystemRoles/MappingProfile.cs | 18 + .../Features/SystemRoles/Requests/Create.cs | 51 + .../Features/SystemRoles/Requests/Delete.cs | 52 + .../Features/SystemRoles/Requests/Edit.cs | 60 + .../Features/SystemRoles/Requests/Get.cs | 50 + .../Features/SystemRoles/Requests/GetAll.cs | 41 + .../Features/SystemRoles/SystemRole.cs | 20 + .../SystemRolesController.cs} | 64 +- .../Terraform/Requests/GetMaxParallelism.cs | 42 +- .../Terraform/Requests/GetVersions.cs | 43 +- .../UserPermissions/Requests/Create.cs | 64 - .../UserPermissions/Requests/Delete.cs | 75 - .../Features/UserPermissions/Requests/Edit.cs | 71 - .../Features/UserPermissions/Requests/Get.cs | 70 - .../UserPermissions/Requests/GetAll.cs | 59 - .../EventHandlers/AuthCacheEventHandler.cs | 35 + .../Features/Users/MappingProfile.cs | 4 +- .../Features/Users/Requests/Create.cs | 49 +- .../Features/Users/Requests/Delete.cs | 41 +- .../Features/Users/Requests/Edit.cs | 44 +- src/Caster.Api/Features/Users/Requests/Get.cs | 35 +- .../Features/Users/Requests/GetAll.cs | 41 +- .../Users/Requests/GetByPermission.cs | 68 - src/Caster.Api/Features/Users/User.cs | 3 +- .../Features/Users/UsersController.cs | 22 +- .../Features/Variables/Requests/Create.cs | 22 +- .../Features/Variables/Requests/Delete.cs | 19 +- .../Features/Variables/Requests/Edit.cs | 22 +- .../Features/Variables/Requests/Get.cs | 17 +- .../Features/Variables/Requests/GetAll.cs | 20 +- .../Requests/Partitions/AssignPartition.cs | 52 +- .../Requests/Partitions/CreatePartition.cs | 47 +- .../Requests/Partitions/DeletePartition.cs | 37 +- .../Vlan/Requests/Partitions/EditPartition.cs | 42 +- .../Vlan/Requests/Partitions/GetPartition.cs | 34 +- .../Vlan/Requests/Partitions/GetPartitions.cs | 35 +- .../Partitions/PartialEditPartition.cs | 42 +- .../Partitions/SetDefaultPartition.cs | 37 +- .../Requests/Partitions/UnassignPartition.cs | 39 +- .../Vlan/Requests/Pools/CreatePool.cs | 44 +- .../Vlan/Requests/Pools/DeletePool.cs | 40 +- .../Features/Vlan/Requests/Pools/EditPool.cs | 39 +- .../Features/Vlan/Requests/Pools/GetPool.cs | 37 +- .../Features/Vlan/Requests/Pools/GetPools.cs | 37 +- .../Vlan/Requests/Pools/PartialEditPool.cs | 39 +- .../Vlan/Requests/Vlans/AcquireVlan.cs | 52 +- .../Requests/Vlans/AddVlansToPartition.cs | 44 +- .../Features/Vlan/Requests/Vlans/GetVlan.cs | 37 +- .../Features/Vlan/Requests/Vlans/GetVlans.cs | 33 +- .../Vlan/Requests/Vlans/PartialEditVlan.cs | 41 +- .../Vlan/Requests/Vlans/ReassignVlans.cs | 43 +- .../Vlan/Requests/Vlans/ReleaseVlan.cs | 41 +- .../Vlans/RemoveVlansFromPartition.cs | 41 +- .../Features/Workspaces/Requests/Create.cs | 55 +- .../Features/Workspaces/Requests/Delete.cs | 49 +- .../Features/Workspaces/Requests/Edit.cs | 38 +- .../Features/Workspaces/Requests/Get.cs | 37 +- .../Features/Workspaces/Requests/GetAll.cs | 38 +- .../Workspaces/Requests/GetByDirectory.cs | 56 +- .../Workspaces/Requests/GetLockingStatus.cs | 34 +- .../Workspaces/Requests/PartialEdit.cs | 41 +- .../Workspaces/Requests/SetLockingStatus.cs | 41 +- src/Caster.Api/Hubs/HubGroups.cs | 4 +- src/Caster.Api/Hubs/ProjectHub.cs | 88 +- .../Authorization/AuthorizationConstants.cs | 10 + .../Authorization/AuthorizationService.cs | 225 +++ .../Authorization/ProjectPermissionClaim.cs | 23 + .../ProjectPermissionRequirement.cs | 70 + .../SystemPermissionRequirement.cs | 42 + .../AuthorizationPolicyExtension.cs | 14 +- .../Extensions/DatabaseExtensions.cs | 39 +- .../Extensions/ValidationExtensions.cs | 35 + .../Identity/IdentityResolver.cs | 12 +- .../Options/ClaimsTransformationOptions.cs | 9 +- .../Infrastructure/Options/SeedDataOptions.cs | 4 +- src/Caster.Api/Startup.cs | 5 +- src/Caster.Api/appsettings.json | 57 +- 248 files changed, 12601 insertions(+), 4523 deletions(-) create mode 100644 docs/Permissions.md create mode 100644 src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.Designer.cs create mode 100644 src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.cs create mode 100644 src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.Designer.cs create mode 100644 src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.cs create mode 100644 src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.Designer.cs create mode 100644 src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.cs create mode 100644 src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.Designer.cs create mode 100644 src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.cs create mode 100644 src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.Designer.cs create mode 100644 src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.cs create mode 100644 src/Caster.Api/Domain/Models/Group.cs create mode 100644 src/Caster.Api/Domain/Models/GroupMembership.cs create mode 100644 src/Caster.Api/Domain/Models/IEntity.cs delete mode 100644 src/Caster.Api/Domain/Models/Permission.cs create mode 100644 src/Caster.Api/Domain/Models/ProjectMembership.cs create mode 100644 src/Caster.Api/Domain/Models/ProjectRole.cs create mode 100644 src/Caster.Api/Domain/Models/SystemRole.cs delete mode 100644 src/Caster.Api/Domain/Models/UserPermission.cs delete mode 100644 src/Caster.Api/Features/Directories/Requests/GetByExercise.cs create mode 100644 src/Caster.Api/Features/Directories/Requests/GetByProject.cs create mode 100644 src/Caster.Api/Features/Groups/EventHandlers/AuthCacheEventHandler.cs create mode 100644 src/Caster.Api/Features/Groups/EventHandlers/SignalREventHandler.cs create mode 100644 src/Caster.Api/Features/Groups/Group.cs rename src/Caster.Api/Features/{UserPermissions/UserPermission.cs => Groups/GroupMembership.cs} (52%) create mode 100644 src/Caster.Api/Features/Groups/GroupsController.cs create mode 100644 src/Caster.Api/Features/Groups/MappingProfile.cs create mode 100644 src/Caster.Api/Features/Groups/Requests/Create.cs create mode 100644 src/Caster.Api/Features/Groups/Requests/CreateMembership.cs create mode 100644 src/Caster.Api/Features/Groups/Requests/Delete.cs create mode 100644 src/Caster.Api/Features/Groups/Requests/DeleteMembership.cs create mode 100644 src/Caster.Api/Features/Groups/Requests/Edit.cs create mode 100644 src/Caster.Api/Features/Groups/Requests/Get.cs create mode 100644 src/Caster.Api/Features/Groups/Requests/GetAll.cs create mode 100644 src/Caster.Api/Features/Groups/Requests/GetMembership.cs create mode 100644 src/Caster.Api/Features/Groups/Requests/GetMemberships.cs delete mode 100644 src/Caster.Api/Features/Permissions/Permission.cs delete mode 100644 src/Caster.Api/Features/Permissions/PermissionsController.cs delete mode 100644 src/Caster.Api/Features/Permissions/Requests/Get.cs delete mode 100644 src/Caster.Api/Features/Permissions/Requests/GetAll.cs delete mode 100644 src/Caster.Api/Features/Permissions/Requests/GetByUser.cs delete mode 100644 src/Caster.Api/Features/Permissions/Requests/GetMine.cs rename src/Caster.Api/Features/{Permissions => ProjectPermissions}/MappingProfile.cs (68%) create mode 100644 src/Caster.Api/Features/ProjectPermissions/ProjectPermissionsController.cs create mode 100644 src/Caster.Api/Features/ProjectPermissions/Requests/GetMine.cs rename src/Caster.Api/Features/{UserPermissions => ProjectRoles}/MappingProfile.cs (50%) create mode 100644 src/Caster.Api/Features/ProjectRoles/ProjectRole.cs create mode 100644 src/Caster.Api/Features/ProjectRoles/ProjectRolesController.cs create mode 100644 src/Caster.Api/Features/ProjectRoles/Requests/Get.cs create mode 100644 src/Caster.Api/Features/ProjectRoles/Requests/GetAll.cs create mode 100644 src/Caster.Api/Features/Projects/EventHandlers/AuthCacheEventHandler.cs create mode 100644 src/Caster.Api/Features/Projects/EventHandlers/SignalREventHandler.cs create mode 100644 src/Caster.Api/Features/Projects/ProjectMembership.cs create mode 100644 src/Caster.Api/Features/Projects/Requests/CreateMembership.cs create mode 100644 src/Caster.Api/Features/Projects/Requests/DeleteMembership.cs create mode 100644 src/Caster.Api/Features/Projects/Requests/EditMembership.cs create mode 100644 src/Caster.Api/Features/Projects/Requests/GetMembership.cs create mode 100644 src/Caster.Api/Features/Projects/Requests/GetMemberships.cs rename src/Caster.Api/Features/{Workspaces/Interfaces/IWorkspaceDeleteRequest.cs => SystemPermissions/MappingProfile.cs} (53%) create mode 100644 src/Caster.Api/Features/SystemPermissions/Requests/GetMine.cs create mode 100644 src/Caster.Api/Features/SystemPermissions/SystemPermissionsController.cs create mode 100644 src/Caster.Api/Features/SystemRoles/EventHandlers/AuthCacheEventHandler.cs create mode 100644 src/Caster.Api/Features/SystemRoles/EventHandlers/SignalREventHandler.cs create mode 100644 src/Caster.Api/Features/SystemRoles/MappingProfile.cs create mode 100644 src/Caster.Api/Features/SystemRoles/Requests/Create.cs create mode 100644 src/Caster.Api/Features/SystemRoles/Requests/Delete.cs create mode 100644 src/Caster.Api/Features/SystemRoles/Requests/Edit.cs create mode 100644 src/Caster.Api/Features/SystemRoles/Requests/Get.cs create mode 100644 src/Caster.Api/Features/SystemRoles/Requests/GetAll.cs create mode 100644 src/Caster.Api/Features/SystemRoles/SystemRole.cs rename src/Caster.Api/Features/{UserPermissions/UserPermissionsController.cs => SystemRoles/SystemRolesController.cs} (52%) delete mode 100644 src/Caster.Api/Features/UserPermissions/Requests/Create.cs delete mode 100644 src/Caster.Api/Features/UserPermissions/Requests/Delete.cs delete mode 100644 src/Caster.Api/Features/UserPermissions/Requests/Edit.cs delete mode 100644 src/Caster.Api/Features/UserPermissions/Requests/Get.cs delete mode 100644 src/Caster.Api/Features/UserPermissions/Requests/GetAll.cs create mode 100644 src/Caster.Api/Features/Users/EventHandlers/AuthCacheEventHandler.cs delete mode 100644 src/Caster.Api/Features/Users/Requests/GetByPermission.cs create mode 100644 src/Caster.Api/Infrastructure/Authorization/AuthorizationConstants.cs create mode 100644 src/Caster.Api/Infrastructure/Authorization/AuthorizationService.cs create mode 100644 src/Caster.Api/Infrastructure/Authorization/ProjectPermissionClaim.cs create mode 100644 src/Caster.Api/Infrastructure/Authorization/ProjectPermissionRequirement.cs create mode 100644 src/Caster.Api/Infrastructure/Authorization/SystemPermissionRequirement.cs diff --git a/Dockerfile b/Dockerfile index 22b1b6e..d373a8f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ # FROM mcr.microsoft.com/dotnet/sdk:8.0 AS dev -ENV ASPNETCORE_URLS=http://*:5000 \ - ASPNETCORE_ENVIRONMENT=DEVELOPMENT +ENV ASPNETCORE_URLS=http://*:5000 +ENV ASPNETCORE_ENVIRONMENT=DEVELOPMENT COPY . /app WORKDIR /app/src/Caster.Api @@ -21,7 +21,7 @@ ENV DOTNET_HOSTBUILDER__RELOADCONFIGCHANGE=false COPY --from=dev /app/dist /app WORKDIR /app -ENV ASPNETCORE_HTTP_PORTS=80 +ENV ASPNETCORE_URLS=http://*:80 EXPOSE 80 CMD [ "dotnet", "Caster.Api.dll" ] diff --git a/docs/Permissions.md b/docs/Permissions.md new file mode 100644 index 0000000..2326ca8 --- /dev/null +++ b/docs/Permissions.md @@ -0,0 +1,127 @@ +As of version 3.4.0, Caster transitioned to a new permissions model, allowing for more granular access control to different features of the application. This document will detail how the new system works. + +# Project Membership + +Projects now have members, and a User will only see a Project on their home page if a Project Membership is created for that User in the Project. Users with appropriate Permissions can access the new Project Management area under Administration or from the Project's dropdown menu to manage access to each Project. + +Users with certain Administrative Permissions can access all Projects, even if they are not Members. + +# Permissions + +Access to features of Caster are governed by sets of Permissions. Permissions can apply globally or on a per Project basis. Examples of global Permissions are: + +- CreateProjects - Allows creation of new Projects +- ViewProjects - Allows viewing all Projects and their Users and Groups +- ManageUsers - Allows for making changes to Users. + +The Administration area now can be accessed by any User with View or Manage Permission to an Administration function (e.g. ViewVLANs, ManageWorkspaces, etc), but only the areas they have Permissions for will be accessible in the sidebar menu. + +There are many more Permissions available. They can be viewed by going to the new Roles section of the Administration area. + +# Roles + +Permissions can be applied to Users by grouping them into Roles. There are two types of Roles in Caster: + +- System Roles - Each User can have a System Role applied to them that gives global Permissions across all of Caster. The three default System Roles are: + + - Administrator - Has all Permissions within the system. + - Content Developer - Has the `CreateProjects` Permissions. Users in this Role can create and manage their own Projects, but not affect any global settings or other User's Projects. + - Observer - Has all `View` Permissions. Users in this Role can view everything in the system, but not make any changes. + + Custom System Roles can be created by Users with the `ManageRoles` Permission that include whatever Permissions are desired for that Role. This can be done in the Roles section of the Administration area. + +- Project Roles - When a User is added to a Project, their Project Membership has a Project Role that applies Permissions to that User only for that specific Project. The three available Project Roles are: + + - Member - Can view and edit all objects within the Project. + - Manager - Can perform all Project actions, including managing User access to the Project. When creating a new Project, the creator is given the `Manager` Role in that Project. + - Observer - Can view all objects within the Project, but not many any changes. + + Custom Project Roles cannot be created. + +Roles can be set on Users in the Users section of the Administration area. + +Roles can also optionally be integrated with your Identity Provider. See Identity Provider Integration below. + +# Groups + +Another new section of Administration is Groups. Groups can be created and users can be added to Groups, creating a Group Membership. When managing access to a Project, you can add Groups to the Project in addition to adding Users. This can make it easier to add the same Users to several Projects with the same Project Role. + +For example, if you want to give a group of Users read-only access to a subset of Projects, the `Observer` System Role would be too broad, as it provides read-only access to all Projects, as well as all Administrative settings. Instead, you could create a Group named `Observers`, add the appropriate Users to the Group, and then add this Group to each desired Project, with the `Observer` Project Role for just those Projects. Now, you can simply add or remove Users from this Group and they will gain or lose read-only access to those Projects. + +Similarly, you can add Groups to certain Projects with Member or Manage Project Roles. + +Groups can also optionally be integrated with your Identity Provider. See Identity Provider Integration below. + +# Seed Data + +The SeedData section of appsettings.json has been changed to support the new model. You can now use this section to add Roles, Users, and Groups on application startup. See appsettings.json for examples. + +SeedData will only add objects if they do not exist. It will not modify existing Roles, Users, or Groups so as not to undo changes made in the application on every restart. It will re-create objects if they are deleted in the application, so be sure to remove them from SeedData if they are no longer wanted. + +# Identity Provider Integration + +Roles and Groups can optionally be integrated with the Identity Provider that is being used to authenticate to Caster. There are new settings under `ClaimsTransformation` to configure this integration. See appsettings.json. This integration is compatible with any Identity Provider that is capable of putting Roles and/or Groups into the auth token. + +## Roles + +If enabled, Roles from the User's auth token will be applied as if the Role was set on the User directly in Caster. The Role must exist in Caster and the name of the Role in the token must match exactly with the name of the Role in the token. + +- UseRolesFromIdp: If true, Roles from the User's auth token will be used. Defaults to true. +- RolesClaimPath: The path within the User's auth token to look for Roles. Defaults to Keycloak's default value of `realm_access.roles`. + + Example: If the defaults are set, Caster will apply the `Content Developer` Role to a User whose token contains the following: + +```json + realm_access { + roles: [ + "Content Developer" + ] + } +``` + +If multiple Roles are present in the token, or if one Role is in the token and one Role is set directly on the User in Caster, the Permissions of all of the Roles will be combined. + +## Groups + +If enabled, Groups from the User's auth token will be applied as if the User was a member of the Group(s) in caster. The Group must exist in Caster and the name of the Group in the token must match exactly with the name of the Group in the token. + +- UseGroupsFromIdP: If true, Groups from the User's auth token will be used. Defaults to true. +- GroupsClaimPath: The path within the User's auth token to look for Groups. Defaults to groups. + +Example: If the defaults are set, Caster will consider a User a member of the Power Users and Demo Groups if their token contains the following: + +```json + groups: [ + "Power Users", + "Demo" + ] +``` + +This will be combined with any Group Memberships that User has had created directly in Caster. + +## Keycloak + +If you are using Keycloak as your Identity Provider, Roles should work by default if you have not changed the default `RolesClaimPath`. You may need to adjust this value if your Keycloak is configured to put Roles in a different location within the token. + +Groups need some additional configuration within Keycloak. In Keycloak, Groups are not added to the token by default. You can enable this by creating a new Client Scope or editing an existing one. The easiest method is to edit the default `roles` Client Scope to add Groups to it. + +- From the Keycloak Administrative console, click `Client Scopes` in the sidebar +- Select the `roles` Client Scope +- Select the `Mappers` tab +- Click `Add Mapper` -> `By configuration` +- Select `Group Membership` +- Set `Name` and `Token Claim Name` to `groups` +- Disable `Full group path` and `Add to ID token` +- Enable `Add to access token` and `Add to userinfo` + +Now your auth tokens should contain a `Groups` array that will include and Groups a User has been added to within Keycloak. If you want to enable this for specific applications only, you should instead add this `Groups` mapper to the specific scopes that those clients use instead of the `roles` scope. + +# Migration + +When moving from a version prior to 3.4.0, the database will be migrated from the old Permissions sytem to the new one. The end result should be no change in access to any existing Users. + +- Any existing Users with the old `SystemAdmin` Permission will be migrated to the new `Administrator` Role +- Any existing Users with the old `ContentDeveloper` Permissions will be migrated to the new `Content Developer` Role +- Any existing Users with either of the old Permissions will be given Project Memberships to all existing Projects in order to retain existing access. You can remove these memberships if you want certain Users to have less access than before. + +Be sure to double check all of your Roles, Project Memberships, and Group Memberships once the migration is complete. diff --git a/src/Caster.Api/Data/CasterContext.cs b/src/Caster.Api/Data/CasterContext.cs index 871aca7..b56a710 100644 --- a/src/Caster.Api/Data/CasterContext.cs +++ b/src/Caster.Api/Data/CasterContext.cs @@ -28,9 +28,7 @@ public CasterContext(DbContextOptions options) : base(options) public DbSet RemovedResources { get; set; } public DbSet Modules { get; set; } public DbSet ModuleVersions { get; set; } - public DbSet Permissions { get; set; } public DbSet Users { get; set; } - public DbSet UserPermissions { get; set; } public DbSet Hosts { get; set; } public DbSet HostMachines { get; set; } public DbSet Designs { get; set; } @@ -39,6 +37,11 @@ public CasterContext(DbContextOptions options) : base(options) public DbSet Vlans { get; set; } public DbSet Partitions { get; set; } public DbSet Pools { get; set; } + public DbSet SystemRoles { get; set; } + public DbSet ProjectRoles { get; set; } + public DbSet ProjectMemberships { get; set; } + public DbSet Groups { get; set; } + public DbSet GroupMemberships { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.Designer.cs b/src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.Designer.cs new file mode 100644 index 0000000..0af5680 --- /dev/null +++ b/src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.Designer.cs @@ -0,0 +1,1421 @@ +// +using System; +using Caster.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Caster.Api.Data.Migrations +{ + [DbContext(typeof(CasterContext))] + [Migration("20241220192002_Granular_Permissions")] + partial class Granular_Permissions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "uuid-ossp"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Caster.Api.Domain.Models.Apply", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Output") + .HasColumnType("text") + .HasColumnName("output"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("RunId") + .IsUnique(); + + b.ToTable("applies"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.ToTable("designs"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.DesignModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DesignId") + .HasColumnType("uuid") + .HasColumnName("design_id"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("ModuleVersion") + .HasColumnType("text") + .HasColumnName("module_version"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ValuesJson") + .HasColumnType("text") + .HasColumnName("values_json"); + + b.HasKey("Id"); + + b.HasIndex("DesignId"); + + b.HasIndex("ModuleId"); + + b.ToTable("design_modules"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AzureDestroyFailureThreshold") + .HasColumnType("integer") + .HasColumnName("azure_destroy_failure_threshold"); + + b.Property("AzureDestroyFailureThresholdEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("azure_destroy_failure_threshold_enabled"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Parallelism") + .HasColumnType("integer") + .HasColumnName("parallelism"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.Property("TerraformVersion") + .HasColumnType("text") + .HasColumnName("terraform_version"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("ProjectId"); + + b.ToTable("directories"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AdministrativelyLocked") + .HasColumnType("boolean") + .HasColumnName("administratively_locked"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("DateSaved") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_saved"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LockedById") + .HasColumnType("uuid") + .HasColumnName("locked_by_id"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.HasIndex("LockedById"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("files"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.FileVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("DateSaved") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_saved"); + + b.Property("DateTagged") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_tagged"); + + b.Property("FileId") + .HasColumnType("uuid") + .HasColumnName("file_id"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Tag") + .HasColumnType("text") + .HasColumnName("tag"); + + b.Property("TaggedById") + .HasColumnType("uuid") + .HasColumnName("tagged_by_id"); + + b.HasKey("Id"); + + b.HasIndex("FileId"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("TaggedById"); + + b.ToTable("file_versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("groups"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("GroupId", "UserId") + .IsUnique(); + + b.ToTable("group_memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Datastore") + .HasColumnType("text") + .HasColumnName("datastore"); + + b.Property("Development") + .HasColumnType("boolean") + .HasColumnName("development"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("MaximumMachines") + .HasColumnType("integer") + .HasColumnName("maximum_machines"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("hosts"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.HostMachine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("HostId") + .HasColumnType("uuid") + .HasColumnName("host_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("HostId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("host_machines"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Module", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_modified"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.HasKey("Id"); + + b.ToTable("modules"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ModuleVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_created"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Outputs") + .HasColumnType("text") + .HasColumnName("outputs"); + + b.Property("UrlLink") + .HasColumnType("text") + .HasColumnName("url_link"); + + b.Property("Variables") + .HasColumnType("text") + .HasColumnName("variables"); + + b.HasKey("Id"); + + b.HasIndex("ModuleId"); + + b.ToTable("module_versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("is_default"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PoolId") + .HasColumnType("uuid") + .HasColumnName("pool_id"); + + b.HasKey("Id"); + + b.HasIndex("PoolId"); + + b.ToTable("partitions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Key") + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("ReadOnly") + .HasColumnType("boolean") + .HasColumnName("read_only"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id"); + + b.HasIndex("Key", "Value") + .IsUnique(); + + b.ToTable("permissions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Plan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Output") + .HasColumnType("text") + .HasColumnName("output"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("RunId") + .IsUnique(); + + b.ToTable("plans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Pool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("is_default"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("pools"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PartitionId") + .HasColumnType("uuid") + .HasColumnName("partition_id"); + + b.HasKey("Id"); + + b.HasIndex("PartitionId"); + + b.ToTable("projects"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValue(new Guid("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4")) + .HasColumnName("role_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId", "GroupId") + .IsUnique(); + + b.ToTable("project_memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AllPermissions") + .HasColumnType("boolean") + .HasColumnName("all_permissions"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("integer[]") + .HasColumnName("permissions"); + + b.HasKey("Id"); + + b.ToTable("project_roles"); + + b.HasData( + new + { + Id = new Guid("1a3f26cd-9d99-4b98-b914-12931e786198"), + AllPermissions = true, + Description = "Can perform all actions on the Project", + Name = "Manager", + Permissions = new int[0] + }, + new + { + Id = new Guid("39aa296e-05ba-4fb0-8d74-c92cf3354c6f"), + AllPermissions = false, + Description = "Has read only access to the Project", + Name = "Observer", + Permissions = new[] { 0 } + }, + new + { + Id = new Guid("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4"), + AllPermissions = false, + Description = "Has read only access to the Project", + Name = "Member", + Permissions = new[] { 0, 1, 3 } + }); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.RemovedResource", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.HasKey("Id"); + + b.ToTable("removed_resources"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("IsDestroy") + .HasColumnType("boolean") + .HasColumnName("is_destroy"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Targets") + .HasColumnType("text") + .HasColumnName("targets"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("runs"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.SystemRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AllPermissions") + .HasColumnType("boolean") + .HasColumnName("all_permissions"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Immutable") + .HasColumnType("boolean") + .HasColumnName("immutable"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("integer[]") + .HasColumnName("permissions"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("system_roles"); + + b.HasData( + new + { + Id = new Guid("f35e8fff-f996-4cba-b303-3ba515ad8d2f"), + AllPermissions = true, + Description = "Can perform all actions.", + Immutable = true, + Name = "Administrator", + Permissions = new int[0] + }, + new + { + Id = new Guid("d80b73c3-95d7-4468-8650-c62bbd082507"), + AllPermissions = false, + Description = "Can create and manage their own Projects.", + Immutable = false, + Name = "Content Developer", + Permissions = new[] { 0 } + }, + new + { + Id = new Guid("1da3027e-725d-4753-9455-a836ed9bdb1e"), + AllPermissions = false, + Description = "Can perform all View actions, but not make any changes.", + Immutable = false, + Name = "Observer", + Permissions = new[] { 1, 6, 8, 10, 12, 14, 16, 18 } + }); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.UserPermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("PermissionId") + .HasColumnType("uuid") + .HasColumnName("permission_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("PermissionId"); + + b.HasIndex("UserId", "PermissionId") + .IsUnique(); + + b.ToTable("user_permissions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Variable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DefaultValue") + .HasColumnType("text") + .HasColumnName("default_value"); + + b.Property("DesignId") + .HasColumnType("uuid") + .HasColumnName("design_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id"); + + b.HasIndex("DesignId"); + + b.ToTable("variables"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Vlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("InUse") + .HasColumnType("boolean") + .HasColumnName("in_use"); + + b.Property("PartitionId") + .HasColumnType("uuid") + .HasColumnName("partition_id"); + + b.Property("PoolId") + .HasColumnType("uuid") + .HasColumnName("pool_id"); + + b.Property("Reserved") + .HasColumnType("boolean") + .HasColumnName("reserved"); + + b.Property("Tag") + .HasColumnType("text") + .HasColumnName("tag"); + + b.Property("VlanId") + .HasColumnType("integer") + .HasColumnName("vlan_id"); + + b.HasKey("Id"); + + b.HasIndex("PartitionId"); + + b.HasIndex("PoolId"); + + b.HasIndex("VlanId"); + + b.ToTable("vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AzureDestroyFailureThreshold") + .HasColumnType("integer") + .HasColumnName("azure_destroy_failure_threshold"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("DynamicHost") + .HasColumnType("boolean") + .HasColumnName("dynamic_host"); + + b.Property("HostId") + .HasColumnType("uuid") + .HasColumnName("host_id"); + + b.Property("LastSynced") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_synced"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Parallelism") + .HasColumnType("integer") + .HasColumnName("parallelism"); + + b.Property("State") + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("StateBackup") + .HasColumnType("text") + .HasColumnName("state_backup"); + + b.Property("SyncErrors") + .HasColumnType("text") + .HasColumnName("sync_errors"); + + b.Property("TerraformVersion") + .HasColumnType("text") + .HasColumnName("terraform_version"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.HasIndex("HostId"); + + b.ToTable("workspaces"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Apply", b => + { + b.HasOne("Caster.Api.Domain.Models.Run", "Run") + .WithOne("Apply") + .HasForeignKey("Caster.Api.Domain.Models.Apply", "RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Designs") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Directory"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.DesignModule", b => + { + b.HasOne("Caster.Api.Domain.Models.Design", "Design") + .WithMany("Modules") + .HasForeignKey("DesignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Module", "Module") + .WithMany() + .HasForeignKey("ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Design"); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany("Directories") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Parent"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Files") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "LockedBy") + .WithMany() + .HasForeignKey("LockedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany("Files") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Directory"); + + b.Navigation("LockedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.FileVersion", b => + { + b.HasOne("Caster.Api.Domain.Models.File", "File") + .WithMany("FileVersions") + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "TaggedBy") + .WithMany() + .HasForeignKey("TaggedById"); + + b.Navigation("File"); + + b.Navigation("ModifiedBy"); + + b.Navigation("TaggedBy"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.GroupMembership", b => + { + b.HasOne("Caster.Api.Domain.Models.Group", "Group") + .WithMany("Memberships") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "User") + .WithMany("GroupMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.HostMachine", b => + { + b.HasOne("Caster.Api.Domain.Models.Host", "Host") + .WithMany("Machines") + .HasForeignKey("HostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Host"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ModuleVersion", b => + { + b.HasOne("Caster.Api.Domain.Models.Module", "Module") + .WithMany("Versions") + .HasForeignKey("ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.HasOne("Caster.Api.Domain.Models.Pool", "Pool") + .WithMany("Partitions") + .HasForeignKey("PoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Pool"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Plan", b => + { + b.HasOne("Caster.Api.Domain.Models.Run", "Run") + .WithOne("Plan") + .HasForeignKey("Caster.Api.Domain.Models.Plan", "RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.HasOne("Caster.Api.Domain.Models.Partition", "Partition") + .WithMany() + .HasForeignKey("PartitionId"); + + b.Navigation("Partition"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectMembership", b => + { + b.HasOne("Caster.Api.Domain.Models.Group", "Group") + .WithMany("ProjectMemberships") + .HasForeignKey("GroupId"); + + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany("Memberships") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.ProjectRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "User") + .WithMany("ProjectMemberships") + .HasForeignKey("UserId"); + + b.Navigation("Group"); + + b.Navigation("Project"); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.HasOne("Caster.Api.Domain.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany("Runs") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.HasOne("Caster.Api.Domain.Models.SystemRole", "Role") + .WithMany() + .HasForeignKey("RoleId"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.UserPermission", b => + { + b.HasOne("Caster.Api.Domain.Models.Permission", "Permission") + .WithMany("UserPermissions") + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "User") + .WithMany("UserPermissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Permission"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Variable", b => + { + b.HasOne("Caster.Api.Domain.Models.Design", "Design") + .WithMany("Variables") + .HasForeignKey("DesignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Design"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Vlan", b => + { + b.HasOne("Caster.Api.Domain.Models.Partition", null) + .WithMany("Vlans") + .HasForeignKey("PartitionId"); + + b.HasOne("Caster.Api.Domain.Models.Pool", null) + .WithMany("Vlans") + .HasForeignKey("PoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Workspaces") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Host", "Host") + .WithMany() + .HasForeignKey("HostId"); + + b.Navigation("Directory"); + + b.Navigation("Host"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.Navigation("Modules"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.Navigation("Children"); + + b.Navigation("Designs"); + + b.Navigation("Files"); + + b.Navigation("Workspaces"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.Navigation("FileVersions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Group", b => + { + b.Navigation("Memberships"); + + b.Navigation("ProjectMemberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.Navigation("Machines"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Module", b => + { + b.Navigation("Versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.Navigation("Vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Permission", b => + { + b.Navigation("UserPermissions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Pool", b => + { + b.Navigation("Partitions"); + + b.Navigation("Vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.Navigation("Directories"); + + b.Navigation("Memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.Navigation("Apply"); + + b.Navigation("Plan"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.Navigation("GroupMemberships"); + + b.Navigation("ProjectMemberships"); + + b.Navigation("UserPermissions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.Navigation("Files"); + + b.Navigation("Runs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.cs b/src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.cs new file mode 100644 index 0000000..3c47fe7 --- /dev/null +++ b/src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.cs @@ -0,0 +1,230 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Caster.Api.Data.Migrations +{ + /// + public partial class Granular_Permissions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "role_id", + table: "users", + type: "uuid", + nullable: true); + + migrationBuilder.CreateTable( + name: "groups", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "uuid_generate_v4()"), + name = table.Column(type: "text", nullable: true), + description = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_groups", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "project_roles", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "uuid_generate_v4()"), + name = table.Column(type: "text", nullable: true), + description = table.Column(type: "text", nullable: true), + all_permissions = table.Column(type: "boolean", nullable: false), + permissions = table.Column(type: "integer[]", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_project_roles", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "system_roles", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "uuid_generate_v4()"), + name = table.Column(type: "text", nullable: true), + description = table.Column(type: "text", nullable: true), + all_permissions = table.Column(type: "boolean", nullable: false), + immutable = table.Column(type: "boolean", nullable: false), + permissions = table.Column(type: "integer[]", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_system_roles", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "group_memberships", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "uuid_generate_v4()"), + group_id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_group_memberships", x => x.id); + table.ForeignKey( + name: "FK_group_memberships_groups_group_id", + column: x => x.group_id, + principalTable: "groups", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_group_memberships_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "project_memberships", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "uuid_generate_v4()"), + project_id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: true), + group_id = table.Column(type: "uuid", nullable: true), + role_id = table.Column(type: "uuid", nullable: false, defaultValue: new Guid("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4")) + }, + constraints: table => + { + table.PrimaryKey("PK_project_memberships", x => x.id); + table.ForeignKey( + name: "FK_project_memberships_groups_group_id", + column: x => x.group_id, + principalTable: "groups", + principalColumn: "id"); + table.ForeignKey( + name: "FK_project_memberships_project_roles_role_id", + column: x => x.role_id, + principalTable: "project_roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_project_memberships_projects_project_id", + column: x => x.project_id, + principalTable: "projects", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_project_memberships_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id"); + }); + + migrationBuilder.InsertData( + table: "project_roles", + columns: new[] { "id", "all_permissions", "description", "name", "permissions" }, + values: new object[,] + { + { new Guid("1a3f26cd-9d99-4b98-b914-12931e786198"), true, "Can perform all actions on the Project", "Manager", new int[0] }, + { new Guid("39aa296e-05ba-4fb0-8d74-c92cf3354c6f"), false, "Has read only access to the Project", "Observer", new[] { 0 } }, + { new Guid("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4"), false, "Has read only access to the Project", "Member", new[] { 0, 1, 3 } } + }); + + migrationBuilder.InsertData( + table: "system_roles", + columns: new[] { "id", "all_permissions", "description", "immutable", "name", "permissions" }, + values: new object[,] + { + { new Guid("1da3027e-725d-4753-9455-a836ed9bdb1e"), false, "Can perform all View actions, but not make any changes.", false, "Observer", new[] { 1, 6, 8, 10, 12, 14, 16, 18 } }, + { new Guid("d80b73c3-95d7-4468-8650-c62bbd082507"), false, "Can create and manage their own Projects.", false, "Content Developer", new[] { 0 } }, + { new Guid("f35e8fff-f996-4cba-b303-3ba515ad8d2f"), true, "Can perform all actions.", true, "Administrator", new int[0] } + }); + + migrationBuilder.CreateIndex( + name: "IX_users_role_id", + table: "users", + column: "role_id"); + + migrationBuilder.CreateIndex( + name: "IX_group_memberships_group_id_user_id", + table: "group_memberships", + columns: new[] { "group_id", "user_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_group_memberships_user_id", + table: "group_memberships", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "IX_project_memberships_group_id", + table: "project_memberships", + column: "group_id"); + + migrationBuilder.CreateIndex( + name: "IX_project_memberships_project_id_user_id_group_id", + table: "project_memberships", + columns: new[] { "project_id", "user_id", "group_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_project_memberships_role_id", + table: "project_memberships", + column: "role_id"); + + migrationBuilder.CreateIndex( + name: "IX_project_memberships_user_id", + table: "project_memberships", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "IX_system_roles_name", + table: "system_roles", + column: "name", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_users_system_roles_role_id", + table: "users", + column: "role_id", + principalTable: "system_roles", + principalColumn: "id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_users_system_roles_role_id", + table: "users"); + + migrationBuilder.DropTable( + name: "group_memberships"); + + migrationBuilder.DropTable( + name: "project_memberships"); + + migrationBuilder.DropTable( + name: "system_roles"); + + migrationBuilder.DropTable( + name: "groups"); + + migrationBuilder.DropTable( + name: "project_roles"); + + migrationBuilder.DropIndex( + name: "IX_users_role_id", + table: "users"); + + migrationBuilder.DropColumn( + name: "role_id", + table: "users"); + } + } +} diff --git a/src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.Designer.cs b/src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.Designer.cs new file mode 100644 index 0000000..841d268 --- /dev/null +++ b/src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.Designer.cs @@ -0,0 +1,1421 @@ +// +using System; +using Caster.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Caster.Api.Data.Migrations +{ + [DbContext(typeof(CasterContext))] + [Migration("20241220192404_Migrate_Old_Permissions")] + partial class Migrate_Old_Permissions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "uuid-ossp"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Caster.Api.Domain.Models.Apply", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Output") + .HasColumnType("text") + .HasColumnName("output"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("RunId") + .IsUnique(); + + b.ToTable("applies"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.ToTable("designs"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.DesignModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DesignId") + .HasColumnType("uuid") + .HasColumnName("design_id"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("ModuleVersion") + .HasColumnType("text") + .HasColumnName("module_version"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ValuesJson") + .HasColumnType("text") + .HasColumnName("values_json"); + + b.HasKey("Id"); + + b.HasIndex("DesignId"); + + b.HasIndex("ModuleId"); + + b.ToTable("design_modules"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AzureDestroyFailureThreshold") + .HasColumnType("integer") + .HasColumnName("azure_destroy_failure_threshold"); + + b.Property("AzureDestroyFailureThresholdEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("azure_destroy_failure_threshold_enabled"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Parallelism") + .HasColumnType("integer") + .HasColumnName("parallelism"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.Property("TerraformVersion") + .HasColumnType("text") + .HasColumnName("terraform_version"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("ProjectId"); + + b.ToTable("directories"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AdministrativelyLocked") + .HasColumnType("boolean") + .HasColumnName("administratively_locked"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("DateSaved") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_saved"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LockedById") + .HasColumnType("uuid") + .HasColumnName("locked_by_id"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.HasIndex("LockedById"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("files"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.FileVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("DateSaved") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_saved"); + + b.Property("DateTagged") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_tagged"); + + b.Property("FileId") + .HasColumnType("uuid") + .HasColumnName("file_id"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Tag") + .HasColumnType("text") + .HasColumnName("tag"); + + b.Property("TaggedById") + .HasColumnType("uuid") + .HasColumnName("tagged_by_id"); + + b.HasKey("Id"); + + b.HasIndex("FileId"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("TaggedById"); + + b.ToTable("file_versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("groups"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("GroupId", "UserId") + .IsUnique(); + + b.ToTable("group_memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Datastore") + .HasColumnType("text") + .HasColumnName("datastore"); + + b.Property("Development") + .HasColumnType("boolean") + .HasColumnName("development"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("MaximumMachines") + .HasColumnType("integer") + .HasColumnName("maximum_machines"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("hosts"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.HostMachine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("HostId") + .HasColumnType("uuid") + .HasColumnName("host_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("HostId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("host_machines"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Module", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_modified"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.HasKey("Id"); + + b.ToTable("modules"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ModuleVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_created"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Outputs") + .HasColumnType("text") + .HasColumnName("outputs"); + + b.Property("UrlLink") + .HasColumnType("text") + .HasColumnName("url_link"); + + b.Property("Variables") + .HasColumnType("text") + .HasColumnName("variables"); + + b.HasKey("Id"); + + b.HasIndex("ModuleId"); + + b.ToTable("module_versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("is_default"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PoolId") + .HasColumnType("uuid") + .HasColumnName("pool_id"); + + b.HasKey("Id"); + + b.HasIndex("PoolId"); + + b.ToTable("partitions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Key") + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("ReadOnly") + .HasColumnType("boolean") + .HasColumnName("read_only"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id"); + + b.HasIndex("Key", "Value") + .IsUnique(); + + b.ToTable("permissions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Plan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Output") + .HasColumnType("text") + .HasColumnName("output"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("RunId") + .IsUnique(); + + b.ToTable("plans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Pool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("is_default"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("pools"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PartitionId") + .HasColumnType("uuid") + .HasColumnName("partition_id"); + + b.HasKey("Id"); + + b.HasIndex("PartitionId"); + + b.ToTable("projects"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValue(new Guid("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4")) + .HasColumnName("role_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId", "GroupId") + .IsUnique(); + + b.ToTable("project_memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AllPermissions") + .HasColumnType("boolean") + .HasColumnName("all_permissions"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("integer[]") + .HasColumnName("permissions"); + + b.HasKey("Id"); + + b.ToTable("project_roles"); + + b.HasData( + new + { + Id = new Guid("1a3f26cd-9d99-4b98-b914-12931e786198"), + AllPermissions = true, + Description = "Can perform all actions on the Project", + Name = "Manager", + Permissions = new int[0] + }, + new + { + Id = new Guid("39aa296e-05ba-4fb0-8d74-c92cf3354c6f"), + AllPermissions = false, + Description = "Has read only access to the Project", + Name = "Observer", + Permissions = new[] { 0 } + }, + new + { + Id = new Guid("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4"), + AllPermissions = false, + Description = "Has read only access to the Project", + Name = "Member", + Permissions = new[] { 0, 1, 3 } + }); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.RemovedResource", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.HasKey("Id"); + + b.ToTable("removed_resources"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("IsDestroy") + .HasColumnType("boolean") + .HasColumnName("is_destroy"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Targets") + .HasColumnType("text") + .HasColumnName("targets"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("runs"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.SystemRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AllPermissions") + .HasColumnType("boolean") + .HasColumnName("all_permissions"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Immutable") + .HasColumnType("boolean") + .HasColumnName("immutable"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("integer[]") + .HasColumnName("permissions"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("system_roles"); + + b.HasData( + new + { + Id = new Guid("f35e8fff-f996-4cba-b303-3ba515ad8d2f"), + AllPermissions = true, + Description = "Can perform all actions.", + Immutable = true, + Name = "Administrator", + Permissions = new int[0] + }, + new + { + Id = new Guid("d80b73c3-95d7-4468-8650-c62bbd082507"), + AllPermissions = false, + Description = "Can create and manage their own Projects.", + Immutable = false, + Name = "Content Developer", + Permissions = new[] { 0 } + }, + new + { + Id = new Guid("1da3027e-725d-4753-9455-a836ed9bdb1e"), + AllPermissions = false, + Description = "Can perform all View actions, but not make any changes.", + Immutable = false, + Name = "Observer", + Permissions = new[] { 1, 6, 8, 10, 12, 14, 16, 18 } + }); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.UserPermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("PermissionId") + .HasColumnType("uuid") + .HasColumnName("permission_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("PermissionId"); + + b.HasIndex("UserId", "PermissionId") + .IsUnique(); + + b.ToTable("user_permissions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Variable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DefaultValue") + .HasColumnType("text") + .HasColumnName("default_value"); + + b.Property("DesignId") + .HasColumnType("uuid") + .HasColumnName("design_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id"); + + b.HasIndex("DesignId"); + + b.ToTable("variables"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Vlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("InUse") + .HasColumnType("boolean") + .HasColumnName("in_use"); + + b.Property("PartitionId") + .HasColumnType("uuid") + .HasColumnName("partition_id"); + + b.Property("PoolId") + .HasColumnType("uuid") + .HasColumnName("pool_id"); + + b.Property("Reserved") + .HasColumnType("boolean") + .HasColumnName("reserved"); + + b.Property("Tag") + .HasColumnType("text") + .HasColumnName("tag"); + + b.Property("VlanId") + .HasColumnType("integer") + .HasColumnName("vlan_id"); + + b.HasKey("Id"); + + b.HasIndex("PartitionId"); + + b.HasIndex("PoolId"); + + b.HasIndex("VlanId"); + + b.ToTable("vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AzureDestroyFailureThreshold") + .HasColumnType("integer") + .HasColumnName("azure_destroy_failure_threshold"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("DynamicHost") + .HasColumnType("boolean") + .HasColumnName("dynamic_host"); + + b.Property("HostId") + .HasColumnType("uuid") + .HasColumnName("host_id"); + + b.Property("LastSynced") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_synced"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Parallelism") + .HasColumnType("integer") + .HasColumnName("parallelism"); + + b.Property("State") + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("StateBackup") + .HasColumnType("text") + .HasColumnName("state_backup"); + + b.Property("SyncErrors") + .HasColumnType("text") + .HasColumnName("sync_errors"); + + b.Property("TerraformVersion") + .HasColumnType("text") + .HasColumnName("terraform_version"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.HasIndex("HostId"); + + b.ToTable("workspaces"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Apply", b => + { + b.HasOne("Caster.Api.Domain.Models.Run", "Run") + .WithOne("Apply") + .HasForeignKey("Caster.Api.Domain.Models.Apply", "RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Designs") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Directory"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.DesignModule", b => + { + b.HasOne("Caster.Api.Domain.Models.Design", "Design") + .WithMany("Modules") + .HasForeignKey("DesignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Module", "Module") + .WithMany() + .HasForeignKey("ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Design"); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany("Directories") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Parent"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Files") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "LockedBy") + .WithMany() + .HasForeignKey("LockedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany("Files") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Directory"); + + b.Navigation("LockedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.FileVersion", b => + { + b.HasOne("Caster.Api.Domain.Models.File", "File") + .WithMany("FileVersions") + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "TaggedBy") + .WithMany() + .HasForeignKey("TaggedById"); + + b.Navigation("File"); + + b.Navigation("ModifiedBy"); + + b.Navigation("TaggedBy"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.GroupMembership", b => + { + b.HasOne("Caster.Api.Domain.Models.Group", "Group") + .WithMany("Memberships") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "User") + .WithMany("GroupMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.HostMachine", b => + { + b.HasOne("Caster.Api.Domain.Models.Host", "Host") + .WithMany("Machines") + .HasForeignKey("HostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Host"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ModuleVersion", b => + { + b.HasOne("Caster.Api.Domain.Models.Module", "Module") + .WithMany("Versions") + .HasForeignKey("ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.HasOne("Caster.Api.Domain.Models.Pool", "Pool") + .WithMany("Partitions") + .HasForeignKey("PoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Pool"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Plan", b => + { + b.HasOne("Caster.Api.Domain.Models.Run", "Run") + .WithOne("Plan") + .HasForeignKey("Caster.Api.Domain.Models.Plan", "RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.HasOne("Caster.Api.Domain.Models.Partition", "Partition") + .WithMany() + .HasForeignKey("PartitionId"); + + b.Navigation("Partition"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectMembership", b => + { + b.HasOne("Caster.Api.Domain.Models.Group", "Group") + .WithMany("ProjectMemberships") + .HasForeignKey("GroupId"); + + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany("Memberships") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.ProjectRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "User") + .WithMany("ProjectMemberships") + .HasForeignKey("UserId"); + + b.Navigation("Group"); + + b.Navigation("Project"); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.HasOne("Caster.Api.Domain.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany("Runs") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.HasOne("Caster.Api.Domain.Models.SystemRole", "Role") + .WithMany() + .HasForeignKey("RoleId"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.UserPermission", b => + { + b.HasOne("Caster.Api.Domain.Models.Permission", "Permission") + .WithMany("UserPermissions") + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "User") + .WithMany("UserPermissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Permission"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Variable", b => + { + b.HasOne("Caster.Api.Domain.Models.Design", "Design") + .WithMany("Variables") + .HasForeignKey("DesignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Design"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Vlan", b => + { + b.HasOne("Caster.Api.Domain.Models.Partition", null) + .WithMany("Vlans") + .HasForeignKey("PartitionId"); + + b.HasOne("Caster.Api.Domain.Models.Pool", null) + .WithMany("Vlans") + .HasForeignKey("PoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Workspaces") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Host", "Host") + .WithMany() + .HasForeignKey("HostId"); + + b.Navigation("Directory"); + + b.Navigation("Host"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.Navigation("Modules"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.Navigation("Children"); + + b.Navigation("Designs"); + + b.Navigation("Files"); + + b.Navigation("Workspaces"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.Navigation("FileVersions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Group", b => + { + b.Navigation("Memberships"); + + b.Navigation("ProjectMemberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.Navigation("Machines"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Module", b => + { + b.Navigation("Versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.Navigation("Vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Permission", b => + { + b.Navigation("UserPermissions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Pool", b => + { + b.Navigation("Partitions"); + + b.Navigation("Vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.Navigation("Directories"); + + b.Navigation("Memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.Navigation("Apply"); + + b.Navigation("Plan"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.Navigation("GroupMemberships"); + + b.Navigation("ProjectMemberships"); + + b.Navigation("UserPermissions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.Navigation("Files"); + + b.Navigation("Runs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.cs b/src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.cs new file mode 100644 index 0000000..986b683 --- /dev/null +++ b/src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Caster.Api.Data.Migrations +{ + /// + public partial class Migrate_Old_Permissions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" + UPDATE users + SET role_id = ( + SELECT id + FROM system_roles + WHERE name = 'Content Developer' + ) + WHERE id IN ( + SELECT user_id + FROM user_permissions + WHERE permission_id = ( + SELECT id + FROM permissions + WHERE key = 'ContentDeveloper' + ) + ) + "); + + migrationBuilder.Sql(@" + UPDATE users + SET role_id = ( + SELECT id + FROM system_roles + WHERE name = 'Administrator' + ) + WHERE id IN ( + SELECT user_id + FROM user_permissions + WHERE permission_id = ( + SELECT id + FROM permissions + WHERE key = 'SystemAdmin' + ) + ) + "); + + migrationBuilder.Sql(@" + INSERT INTO project_memberships (user_id, project_id) + SELECT + u.id, + p.id + FROM + users u + INNER JOIN + user_permissions up ON u.id = up.user_id + CROSS JOIN + projects p; + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@"UPDATE users SET role_id = null"); + } + } +} diff --git a/src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.Designer.cs b/src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.Designer.cs new file mode 100644 index 0000000..4a7a0c2 --- /dev/null +++ b/src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.Designer.cs @@ -0,0 +1,1337 @@ +// +using System; +using Caster.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Caster.Api.Data.Migrations +{ + [DbContext(typeof(CasterContext))] + [Migration("20241220200146_Remove_Old_Permissions")] + partial class Remove_Old_Permissions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "uuid-ossp"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Caster.Api.Domain.Models.Apply", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Output") + .HasColumnType("text") + .HasColumnName("output"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("RunId") + .IsUnique(); + + b.ToTable("applies"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.ToTable("designs"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.DesignModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DesignId") + .HasColumnType("uuid") + .HasColumnName("design_id"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("ModuleVersion") + .HasColumnType("text") + .HasColumnName("module_version"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ValuesJson") + .HasColumnType("text") + .HasColumnName("values_json"); + + b.HasKey("Id"); + + b.HasIndex("DesignId"); + + b.HasIndex("ModuleId"); + + b.ToTable("design_modules"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AzureDestroyFailureThreshold") + .HasColumnType("integer") + .HasColumnName("azure_destroy_failure_threshold"); + + b.Property("AzureDestroyFailureThresholdEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("azure_destroy_failure_threshold_enabled"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Parallelism") + .HasColumnType("integer") + .HasColumnName("parallelism"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.Property("TerraformVersion") + .HasColumnType("text") + .HasColumnName("terraform_version"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("ProjectId"); + + b.ToTable("directories"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AdministrativelyLocked") + .HasColumnType("boolean") + .HasColumnName("administratively_locked"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("DateSaved") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_saved"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LockedById") + .HasColumnType("uuid") + .HasColumnName("locked_by_id"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.HasIndex("LockedById"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("files"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.FileVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("DateSaved") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_saved"); + + b.Property("DateTagged") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_tagged"); + + b.Property("FileId") + .HasColumnType("uuid") + .HasColumnName("file_id"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Tag") + .HasColumnType("text") + .HasColumnName("tag"); + + b.Property("TaggedById") + .HasColumnType("uuid") + .HasColumnName("tagged_by_id"); + + b.HasKey("Id"); + + b.HasIndex("FileId"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("TaggedById"); + + b.ToTable("file_versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("groups"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("GroupId", "UserId") + .IsUnique(); + + b.ToTable("group_memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Datastore") + .HasColumnType("text") + .HasColumnName("datastore"); + + b.Property("Development") + .HasColumnType("boolean") + .HasColumnName("development"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("MaximumMachines") + .HasColumnType("integer") + .HasColumnName("maximum_machines"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("hosts"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.HostMachine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("HostId") + .HasColumnType("uuid") + .HasColumnName("host_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("HostId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("host_machines"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Module", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_modified"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.HasKey("Id"); + + b.ToTable("modules"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ModuleVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_created"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Outputs") + .HasColumnType("text") + .HasColumnName("outputs"); + + b.Property("UrlLink") + .HasColumnType("text") + .HasColumnName("url_link"); + + b.Property("Variables") + .HasColumnType("text") + .HasColumnName("variables"); + + b.HasKey("Id"); + + b.HasIndex("ModuleId"); + + b.ToTable("module_versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("is_default"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PoolId") + .HasColumnType("uuid") + .HasColumnName("pool_id"); + + b.HasKey("Id"); + + b.HasIndex("PoolId"); + + b.ToTable("partitions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Plan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Output") + .HasColumnType("text") + .HasColumnName("output"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("RunId") + .IsUnique(); + + b.ToTable("plans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Pool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("is_default"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("pools"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PartitionId") + .HasColumnType("uuid") + .HasColumnName("partition_id"); + + b.HasKey("Id"); + + b.HasIndex("PartitionId"); + + b.ToTable("projects"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValue(new Guid("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4")) + .HasColumnName("role_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId", "GroupId") + .IsUnique(); + + b.ToTable("project_memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AllPermissions") + .HasColumnType("boolean") + .HasColumnName("all_permissions"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("integer[]") + .HasColumnName("permissions"); + + b.HasKey("Id"); + + b.ToTable("project_roles"); + + b.HasData( + new + { + Id = new Guid("1a3f26cd-9d99-4b98-b914-12931e786198"), + AllPermissions = true, + Description = "Can perform all actions on the Project", + Name = "Manager", + Permissions = new int[0] + }, + new + { + Id = new Guid("39aa296e-05ba-4fb0-8d74-c92cf3354c6f"), + AllPermissions = false, + Description = "Has read only access to the Project", + Name = "Observer", + Permissions = new[] { 0 } + }, + new + { + Id = new Guid("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4"), + AllPermissions = false, + Description = "Has read only access to the Project", + Name = "Member", + Permissions = new[] { 0, 1, 3 } + }); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.RemovedResource", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.HasKey("Id"); + + b.ToTable("removed_resources"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("IsDestroy") + .HasColumnType("boolean") + .HasColumnName("is_destroy"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Targets") + .HasColumnType("text") + .HasColumnName("targets"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("runs"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.SystemRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AllPermissions") + .HasColumnType("boolean") + .HasColumnName("all_permissions"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Immutable") + .HasColumnType("boolean") + .HasColumnName("immutable"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("integer[]") + .HasColumnName("permissions"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("system_roles"); + + b.HasData( + new + { + Id = new Guid("f35e8fff-f996-4cba-b303-3ba515ad8d2f"), + AllPermissions = true, + Description = "Can perform all actions.", + Immutable = true, + Name = "Administrator", + Permissions = new int[0] + }, + new + { + Id = new Guid("d80b73c3-95d7-4468-8650-c62bbd082507"), + AllPermissions = false, + Description = "Can create and manage their own Projects.", + Immutable = false, + Name = "Content Developer", + Permissions = new[] { 0 } + }, + new + { + Id = new Guid("1da3027e-725d-4753-9455-a836ed9bdb1e"), + AllPermissions = false, + Description = "Can perform all View actions, but not make any changes.", + Immutable = false, + Name = "Observer", + Permissions = new[] { 1, 6, 8, 10, 12, 14, 16, 18 } + }); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Variable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DefaultValue") + .HasColumnType("text") + .HasColumnName("default_value"); + + b.Property("DesignId") + .HasColumnType("uuid") + .HasColumnName("design_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id"); + + b.HasIndex("DesignId"); + + b.ToTable("variables"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Vlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("InUse") + .HasColumnType("boolean") + .HasColumnName("in_use"); + + b.Property("PartitionId") + .HasColumnType("uuid") + .HasColumnName("partition_id"); + + b.Property("PoolId") + .HasColumnType("uuid") + .HasColumnName("pool_id"); + + b.Property("Reserved") + .HasColumnType("boolean") + .HasColumnName("reserved"); + + b.Property("Tag") + .HasColumnType("text") + .HasColumnName("tag"); + + b.Property("VlanId") + .HasColumnType("integer") + .HasColumnName("vlan_id"); + + b.HasKey("Id"); + + b.HasIndex("PartitionId"); + + b.HasIndex("PoolId"); + + b.HasIndex("VlanId"); + + b.ToTable("vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AzureDestroyFailureThreshold") + .HasColumnType("integer") + .HasColumnName("azure_destroy_failure_threshold"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("DynamicHost") + .HasColumnType("boolean") + .HasColumnName("dynamic_host"); + + b.Property("HostId") + .HasColumnType("uuid") + .HasColumnName("host_id"); + + b.Property("LastSynced") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_synced"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Parallelism") + .HasColumnType("integer") + .HasColumnName("parallelism"); + + b.Property("State") + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("StateBackup") + .HasColumnType("text") + .HasColumnName("state_backup"); + + b.Property("SyncErrors") + .HasColumnType("text") + .HasColumnName("sync_errors"); + + b.Property("TerraformVersion") + .HasColumnType("text") + .HasColumnName("terraform_version"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.HasIndex("HostId"); + + b.ToTable("workspaces"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Apply", b => + { + b.HasOne("Caster.Api.Domain.Models.Run", "Run") + .WithOne("Apply") + .HasForeignKey("Caster.Api.Domain.Models.Apply", "RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Designs") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Directory"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.DesignModule", b => + { + b.HasOne("Caster.Api.Domain.Models.Design", "Design") + .WithMany("Modules") + .HasForeignKey("DesignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Module", "Module") + .WithMany() + .HasForeignKey("ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Design"); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany("Directories") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Parent"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Files") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "LockedBy") + .WithMany() + .HasForeignKey("LockedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany("Files") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Directory"); + + b.Navigation("LockedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.FileVersion", b => + { + b.HasOne("Caster.Api.Domain.Models.File", "File") + .WithMany("FileVersions") + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "TaggedBy") + .WithMany() + .HasForeignKey("TaggedById"); + + b.Navigation("File"); + + b.Navigation("ModifiedBy"); + + b.Navigation("TaggedBy"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.GroupMembership", b => + { + b.HasOne("Caster.Api.Domain.Models.Group", "Group") + .WithMany("Memberships") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "User") + .WithMany("GroupMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.HostMachine", b => + { + b.HasOne("Caster.Api.Domain.Models.Host", "Host") + .WithMany("Machines") + .HasForeignKey("HostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Host"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ModuleVersion", b => + { + b.HasOne("Caster.Api.Domain.Models.Module", "Module") + .WithMany("Versions") + .HasForeignKey("ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.HasOne("Caster.Api.Domain.Models.Pool", "Pool") + .WithMany("Partitions") + .HasForeignKey("PoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Pool"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Plan", b => + { + b.HasOne("Caster.Api.Domain.Models.Run", "Run") + .WithOne("Plan") + .HasForeignKey("Caster.Api.Domain.Models.Plan", "RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.HasOne("Caster.Api.Domain.Models.Partition", "Partition") + .WithMany() + .HasForeignKey("PartitionId"); + + b.Navigation("Partition"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectMembership", b => + { + b.HasOne("Caster.Api.Domain.Models.Group", "Group") + .WithMany("ProjectMemberships") + .HasForeignKey("GroupId"); + + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany("Memberships") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.ProjectRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "User") + .WithMany("ProjectMemberships") + .HasForeignKey("UserId"); + + b.Navigation("Group"); + + b.Navigation("Project"); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.HasOne("Caster.Api.Domain.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany("Runs") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.HasOne("Caster.Api.Domain.Models.SystemRole", "Role") + .WithMany() + .HasForeignKey("RoleId"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Variable", b => + { + b.HasOne("Caster.Api.Domain.Models.Design", "Design") + .WithMany("Variables") + .HasForeignKey("DesignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Design"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Vlan", b => + { + b.HasOne("Caster.Api.Domain.Models.Partition", null) + .WithMany("Vlans") + .HasForeignKey("PartitionId"); + + b.HasOne("Caster.Api.Domain.Models.Pool", null) + .WithMany("Vlans") + .HasForeignKey("PoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Workspaces") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Host", "Host") + .WithMany() + .HasForeignKey("HostId"); + + b.Navigation("Directory"); + + b.Navigation("Host"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.Navigation("Modules"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.Navigation("Children"); + + b.Navigation("Designs"); + + b.Navigation("Files"); + + b.Navigation("Workspaces"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.Navigation("FileVersions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Group", b => + { + b.Navigation("Memberships"); + + b.Navigation("ProjectMemberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.Navigation("Machines"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Module", b => + { + b.Navigation("Versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.Navigation("Vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Pool", b => + { + b.Navigation("Partitions"); + + b.Navigation("Vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.Navigation("Directories"); + + b.Navigation("Memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.Navigation("Apply"); + + b.Navigation("Plan"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.Navigation("GroupMemberships"); + + b.Navigation("ProjectMemberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.Navigation("Files"); + + b.Navigation("Runs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.cs b/src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.cs new file mode 100644 index 0000000..77f227c --- /dev/null +++ b/src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.cs @@ -0,0 +1,82 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Caster.Api.Data.Migrations +{ + /// + public partial class Remove_Old_Permissions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "user_permissions"); + + migrationBuilder.DropTable( + name: "permissions"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "permissions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "uuid_generate_v4()"), + description = table.Column(type: "text", nullable: true), + key = table.Column(type: "text", nullable: true), + read_only = table.Column(type: "boolean", nullable: false), + value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_permissions", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "user_permissions", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false, defaultValueSql: "uuid_generate_v4()"), + permission_id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_user_permissions", x => x.id); + table.ForeignKey( + name: "FK_user_permissions_permissions_permission_id", + column: x => x.permission_id, + principalTable: "permissions", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_user_permissions_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_permissions_key_value", + table: "permissions", + columns: new[] { "key", "value" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_user_permissions_permission_id", + table: "user_permissions", + column: "permission_id"); + + migrationBuilder.CreateIndex( + name: "IX_user_permissions_user_id_permission_id", + table: "user_permissions", + columns: new[] { "user_id", "permission_id" }, + unique: true); + } + } +} diff --git a/src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.Designer.cs b/src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.Designer.cs new file mode 100644 index 0000000..ea7c3a3 --- /dev/null +++ b/src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.Designer.cs @@ -0,0 +1,1340 @@ +// +using System; +using Caster.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Caster.Api.Data.Migrations +{ + [DbContext(typeof(CasterContext))] + [Migration("20241220202800_Unique_Group_Names")] + partial class Unique_Group_Names + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "uuid-ossp"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Caster.Api.Domain.Models.Apply", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Output") + .HasColumnType("text") + .HasColumnName("output"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("RunId") + .IsUnique(); + + b.ToTable("applies"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.ToTable("designs"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.DesignModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DesignId") + .HasColumnType("uuid") + .HasColumnName("design_id"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("ModuleVersion") + .HasColumnType("text") + .HasColumnName("module_version"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ValuesJson") + .HasColumnType("text") + .HasColumnName("values_json"); + + b.HasKey("Id"); + + b.HasIndex("DesignId"); + + b.HasIndex("ModuleId"); + + b.ToTable("design_modules"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AzureDestroyFailureThreshold") + .HasColumnType("integer") + .HasColumnName("azure_destroy_failure_threshold"); + + b.Property("AzureDestroyFailureThresholdEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("azure_destroy_failure_threshold_enabled"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Parallelism") + .HasColumnType("integer") + .HasColumnName("parallelism"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.Property("TerraformVersion") + .HasColumnType("text") + .HasColumnName("terraform_version"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("ProjectId"); + + b.ToTable("directories"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AdministrativelyLocked") + .HasColumnType("boolean") + .HasColumnName("administratively_locked"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("DateSaved") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_saved"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LockedById") + .HasColumnType("uuid") + .HasColumnName("locked_by_id"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.HasIndex("LockedById"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("files"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.FileVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("DateSaved") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_saved"); + + b.Property("DateTagged") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_tagged"); + + b.Property("FileId") + .HasColumnType("uuid") + .HasColumnName("file_id"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Tag") + .HasColumnType("text") + .HasColumnName("tag"); + + b.Property("TaggedById") + .HasColumnType("uuid") + .HasColumnName("tagged_by_id"); + + b.HasKey("Id"); + + b.HasIndex("FileId"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("TaggedById"); + + b.ToTable("file_versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("groups"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("GroupId", "UserId") + .IsUnique(); + + b.ToTable("group_memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Datastore") + .HasColumnType("text") + .HasColumnName("datastore"); + + b.Property("Development") + .HasColumnType("boolean") + .HasColumnName("development"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("MaximumMachines") + .HasColumnType("integer") + .HasColumnName("maximum_machines"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("hosts"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.HostMachine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("HostId") + .HasColumnType("uuid") + .HasColumnName("host_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("HostId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("host_machines"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Module", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_modified"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.HasKey("Id"); + + b.ToTable("modules"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ModuleVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_created"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Outputs") + .HasColumnType("text") + .HasColumnName("outputs"); + + b.Property("UrlLink") + .HasColumnType("text") + .HasColumnName("url_link"); + + b.Property("Variables") + .HasColumnType("text") + .HasColumnName("variables"); + + b.HasKey("Id"); + + b.HasIndex("ModuleId"); + + b.ToTable("module_versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("is_default"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PoolId") + .HasColumnType("uuid") + .HasColumnName("pool_id"); + + b.HasKey("Id"); + + b.HasIndex("PoolId"); + + b.ToTable("partitions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Plan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Output") + .HasColumnType("text") + .HasColumnName("output"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("RunId") + .IsUnique(); + + b.ToTable("plans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Pool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("is_default"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("pools"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PartitionId") + .HasColumnType("uuid") + .HasColumnName("partition_id"); + + b.HasKey("Id"); + + b.HasIndex("PartitionId"); + + b.ToTable("projects"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValue(new Guid("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4")) + .HasColumnName("role_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId", "GroupId") + .IsUnique(); + + b.ToTable("project_memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AllPermissions") + .HasColumnType("boolean") + .HasColumnName("all_permissions"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("integer[]") + .HasColumnName("permissions"); + + b.HasKey("Id"); + + b.ToTable("project_roles"); + + b.HasData( + new + { + Id = new Guid("1a3f26cd-9d99-4b98-b914-12931e786198"), + AllPermissions = true, + Description = "Can perform all actions on the Project", + Name = "Manager", + Permissions = new int[0] + }, + new + { + Id = new Guid("39aa296e-05ba-4fb0-8d74-c92cf3354c6f"), + AllPermissions = false, + Description = "Has read only access to the Project", + Name = "Observer", + Permissions = new[] { 0 } + }, + new + { + Id = new Guid("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4"), + AllPermissions = false, + Description = "Has read only access to the Project", + Name = "Member", + Permissions = new[] { 0, 1, 3 } + }); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.RemovedResource", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.HasKey("Id"); + + b.ToTable("removed_resources"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("IsDestroy") + .HasColumnType("boolean") + .HasColumnName("is_destroy"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Targets") + .HasColumnType("text") + .HasColumnName("targets"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("runs"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.SystemRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AllPermissions") + .HasColumnType("boolean") + .HasColumnName("all_permissions"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Immutable") + .HasColumnType("boolean") + .HasColumnName("immutable"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("integer[]") + .HasColumnName("permissions"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("system_roles"); + + b.HasData( + new + { + Id = new Guid("f35e8fff-f996-4cba-b303-3ba515ad8d2f"), + AllPermissions = true, + Description = "Can perform all actions.", + Immutable = true, + Name = "Administrator", + Permissions = new int[0] + }, + new + { + Id = new Guid("d80b73c3-95d7-4468-8650-c62bbd082507"), + AllPermissions = false, + Description = "Can create and manage their own Projects.", + Immutable = false, + Name = "Content Developer", + Permissions = new[] { 0 } + }, + new + { + Id = new Guid("1da3027e-725d-4753-9455-a836ed9bdb1e"), + AllPermissions = false, + Description = "Can perform all View actions, but not make any changes.", + Immutable = false, + Name = "Observer", + Permissions = new[] { 1, 6, 8, 10, 12, 14, 16, 18 } + }); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Variable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DefaultValue") + .HasColumnType("text") + .HasColumnName("default_value"); + + b.Property("DesignId") + .HasColumnType("uuid") + .HasColumnName("design_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id"); + + b.HasIndex("DesignId"); + + b.ToTable("variables"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Vlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("InUse") + .HasColumnType("boolean") + .HasColumnName("in_use"); + + b.Property("PartitionId") + .HasColumnType("uuid") + .HasColumnName("partition_id"); + + b.Property("PoolId") + .HasColumnType("uuid") + .HasColumnName("pool_id"); + + b.Property("Reserved") + .HasColumnType("boolean") + .HasColumnName("reserved"); + + b.Property("Tag") + .HasColumnType("text") + .HasColumnName("tag"); + + b.Property("VlanId") + .HasColumnType("integer") + .HasColumnName("vlan_id"); + + b.HasKey("Id"); + + b.HasIndex("PartitionId"); + + b.HasIndex("PoolId"); + + b.HasIndex("VlanId"); + + b.ToTable("vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AzureDestroyFailureThreshold") + .HasColumnType("integer") + .HasColumnName("azure_destroy_failure_threshold"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("DynamicHost") + .HasColumnType("boolean") + .HasColumnName("dynamic_host"); + + b.Property("HostId") + .HasColumnType("uuid") + .HasColumnName("host_id"); + + b.Property("LastSynced") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_synced"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Parallelism") + .HasColumnType("integer") + .HasColumnName("parallelism"); + + b.Property("State") + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("StateBackup") + .HasColumnType("text") + .HasColumnName("state_backup"); + + b.Property("SyncErrors") + .HasColumnType("text") + .HasColumnName("sync_errors"); + + b.Property("TerraformVersion") + .HasColumnType("text") + .HasColumnName("terraform_version"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.HasIndex("HostId"); + + b.ToTable("workspaces"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Apply", b => + { + b.HasOne("Caster.Api.Domain.Models.Run", "Run") + .WithOne("Apply") + .HasForeignKey("Caster.Api.Domain.Models.Apply", "RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Designs") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Directory"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.DesignModule", b => + { + b.HasOne("Caster.Api.Domain.Models.Design", "Design") + .WithMany("Modules") + .HasForeignKey("DesignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Module", "Module") + .WithMany() + .HasForeignKey("ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Design"); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany("Directories") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Parent"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Files") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "LockedBy") + .WithMany() + .HasForeignKey("LockedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany("Files") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Directory"); + + b.Navigation("LockedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.FileVersion", b => + { + b.HasOne("Caster.Api.Domain.Models.File", "File") + .WithMany("FileVersions") + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "TaggedBy") + .WithMany() + .HasForeignKey("TaggedById"); + + b.Navigation("File"); + + b.Navigation("ModifiedBy"); + + b.Navigation("TaggedBy"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.GroupMembership", b => + { + b.HasOne("Caster.Api.Domain.Models.Group", "Group") + .WithMany("Memberships") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "User") + .WithMany("GroupMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.HostMachine", b => + { + b.HasOne("Caster.Api.Domain.Models.Host", "Host") + .WithMany("Machines") + .HasForeignKey("HostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Host"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ModuleVersion", b => + { + b.HasOne("Caster.Api.Domain.Models.Module", "Module") + .WithMany("Versions") + .HasForeignKey("ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.HasOne("Caster.Api.Domain.Models.Pool", "Pool") + .WithMany("Partitions") + .HasForeignKey("PoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Pool"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Plan", b => + { + b.HasOne("Caster.Api.Domain.Models.Run", "Run") + .WithOne("Plan") + .HasForeignKey("Caster.Api.Domain.Models.Plan", "RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.HasOne("Caster.Api.Domain.Models.Partition", "Partition") + .WithMany() + .HasForeignKey("PartitionId"); + + b.Navigation("Partition"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectMembership", b => + { + b.HasOne("Caster.Api.Domain.Models.Group", "Group") + .WithMany("ProjectMemberships") + .HasForeignKey("GroupId"); + + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany("Memberships") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.ProjectRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "User") + .WithMany("ProjectMemberships") + .HasForeignKey("UserId"); + + b.Navigation("Group"); + + b.Navigation("Project"); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.HasOne("Caster.Api.Domain.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany("Runs") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.HasOne("Caster.Api.Domain.Models.SystemRole", "Role") + .WithMany() + .HasForeignKey("RoleId"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Variable", b => + { + b.HasOne("Caster.Api.Domain.Models.Design", "Design") + .WithMany("Variables") + .HasForeignKey("DesignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Design"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Vlan", b => + { + b.HasOne("Caster.Api.Domain.Models.Partition", null) + .WithMany("Vlans") + .HasForeignKey("PartitionId"); + + b.HasOne("Caster.Api.Domain.Models.Pool", null) + .WithMany("Vlans") + .HasForeignKey("PoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Workspaces") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Host", "Host") + .WithMany() + .HasForeignKey("HostId"); + + b.Navigation("Directory"); + + b.Navigation("Host"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.Navigation("Modules"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.Navigation("Children"); + + b.Navigation("Designs"); + + b.Navigation("Files"); + + b.Navigation("Workspaces"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.Navigation("FileVersions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Group", b => + { + b.Navigation("Memberships"); + + b.Navigation("ProjectMemberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.Navigation("Machines"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Module", b => + { + b.Navigation("Versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.Navigation("Vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Pool", b => + { + b.Navigation("Partitions"); + + b.Navigation("Vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.Navigation("Directories"); + + b.Navigation("Memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.Navigation("Apply"); + + b.Navigation("Plan"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.Navigation("GroupMemberships"); + + b.Navigation("ProjectMemberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.Navigation("Files"); + + b.Navigation("Runs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.cs b/src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.cs new file mode 100644 index 0000000..88ef018 --- /dev/null +++ b/src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Caster.Api.Data.Migrations +{ + /// + public partial class Unique_Group_Names : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_groups_name", + table: "groups", + column: "name", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_groups_name", + table: "groups"); + } + } +} diff --git a/src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.Designer.cs b/src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.Designer.cs new file mode 100644 index 0000000..e4befeb --- /dev/null +++ b/src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.Designer.cs @@ -0,0 +1,1340 @@ +// +using System; +using Caster.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Caster.Api.Data.Migrations +{ + [DbContext(typeof(CasterContext))] + [Migration("20250102200043_Add_Import_Resources")] + partial class Add_Import_Resources + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "uuid-ossp"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Caster.Api.Domain.Models.Apply", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Output") + .HasColumnType("text") + .HasColumnName("output"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("RunId") + .IsUnique(); + + b.ToTable("applies"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.ToTable("designs"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.DesignModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DesignId") + .HasColumnType("uuid") + .HasColumnName("design_id"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("ModuleVersion") + .HasColumnType("text") + .HasColumnName("module_version"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ValuesJson") + .HasColumnType("text") + .HasColumnName("values_json"); + + b.HasKey("Id"); + + b.HasIndex("DesignId"); + + b.HasIndex("ModuleId"); + + b.ToTable("design_modules"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AzureDestroyFailureThreshold") + .HasColumnType("integer") + .HasColumnName("azure_destroy_failure_threshold"); + + b.Property("AzureDestroyFailureThresholdEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("azure_destroy_failure_threshold_enabled"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Parallelism") + .HasColumnType("integer") + .HasColumnName("parallelism"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.Property("TerraformVersion") + .HasColumnType("text") + .HasColumnName("terraform_version"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("ProjectId"); + + b.ToTable("directories"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AdministrativelyLocked") + .HasColumnType("boolean") + .HasColumnName("administratively_locked"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("DateSaved") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_saved"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LockedById") + .HasColumnType("uuid") + .HasColumnName("locked_by_id"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.HasIndex("LockedById"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("files"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.FileVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("DateSaved") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_saved"); + + b.Property("DateTagged") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_tagged"); + + b.Property("FileId") + .HasColumnType("uuid") + .HasColumnName("file_id"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Tag") + .HasColumnType("text") + .HasColumnName("tag"); + + b.Property("TaggedById") + .HasColumnType("uuid") + .HasColumnName("tagged_by_id"); + + b.HasKey("Id"); + + b.HasIndex("FileId"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("TaggedById"); + + b.ToTable("file_versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("groups"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("GroupId", "UserId") + .IsUnique(); + + b.ToTable("group_memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Datastore") + .HasColumnType("text") + .HasColumnName("datastore"); + + b.Property("Development") + .HasColumnType("boolean") + .HasColumnName("development"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("MaximumMachines") + .HasColumnType("integer") + .HasColumnName("maximum_machines"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("hosts"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.HostMachine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("HostId") + .HasColumnType("uuid") + .HasColumnName("host_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("HostId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("host_machines"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Module", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_modified"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.HasKey("Id"); + + b.ToTable("modules"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ModuleVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_created"); + + b.Property("ModuleId") + .HasColumnType("uuid") + .HasColumnName("module_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Outputs") + .HasColumnType("text") + .HasColumnName("outputs"); + + b.Property("UrlLink") + .HasColumnType("text") + .HasColumnName("url_link"); + + b.Property("Variables") + .HasColumnType("text") + .HasColumnName("variables"); + + b.HasKey("Id"); + + b.HasIndex("ModuleId"); + + b.ToTable("module_versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("is_default"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PoolId") + .HasColumnType("uuid") + .HasColumnName("pool_id"); + + b.HasKey("Id"); + + b.HasIndex("PoolId"); + + b.ToTable("partitions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Plan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Output") + .HasColumnType("text") + .HasColumnName("output"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("RunId") + .IsUnique(); + + b.ToTable("plans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Pool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("is_default"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("pools"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PartitionId") + .HasColumnType("uuid") + .HasColumnName("partition_id"); + + b.HasKey("Id"); + + b.HasIndex("PartitionId"); + + b.ToTable("projects"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValue(new Guid("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4")) + .HasColumnName("role_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId", "GroupId") + .IsUnique(); + + b.ToTable("project_memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AllPermissions") + .HasColumnType("boolean") + .HasColumnName("all_permissions"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("integer[]") + .HasColumnName("permissions"); + + b.HasKey("Id"); + + b.ToTable("project_roles"); + + b.HasData( + new + { + Id = new Guid("1a3f26cd-9d99-4b98-b914-12931e786198"), + AllPermissions = true, + Description = "Can perform all actions on the Project", + Name = "Manager", + Permissions = new int[0] + }, + new + { + Id = new Guid("39aa296e-05ba-4fb0-8d74-c92cf3354c6f"), + AllPermissions = false, + Description = "Has read only access to the Project", + Name = "Observer", + Permissions = new[] { 0 } + }, + new + { + Id = new Guid("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4"), + AllPermissions = false, + Description = "Has read only access to the Project", + Name = "Member", + Permissions = new[] { 0, 1, 3 } + }); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.RemovedResource", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.HasKey("Id"); + + b.ToTable("removed_resources"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("IsDestroy") + .HasColumnType("boolean") + .HasColumnName("is_destroy"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Targets") + .HasColumnType("text") + .HasColumnName("targets"); + + b.Property("WorkspaceId") + .HasColumnType("uuid") + .HasColumnName("workspace_id"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ModifiedById"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("runs"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.SystemRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AllPermissions") + .HasColumnType("boolean") + .HasColumnName("all_permissions"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Immutable") + .HasColumnType("boolean") + .HasColumnName("immutable"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("integer[]") + .HasColumnName("permissions"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("system_roles"); + + b.HasData( + new + { + Id = new Guid("f35e8fff-f996-4cba-b303-3ba515ad8d2f"), + AllPermissions = true, + Description = "Can perform all actions.", + Immutable = true, + Name = "Administrator", + Permissions = new int[0] + }, + new + { + Id = new Guid("d80b73c3-95d7-4468-8650-c62bbd082507"), + AllPermissions = false, + Description = "Can create and manage their own Projects.", + Immutable = false, + Name = "Content Developer", + Permissions = new[] { 0 } + }, + new + { + Id = new Guid("1da3027e-725d-4753-9455-a836ed9bdb1e"), + AllPermissions = false, + Description = "Can perform all View actions, but not make any changes.", + Immutable = false, + Name = "Observer", + Permissions = new[] { 1, 7, 9, 11, 13, 15, 17, 19 } + }); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("RoleId") + .HasColumnType("uuid") + .HasColumnName("role_id"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Variable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("DefaultValue") + .HasColumnType("text") + .HasColumnName("default_value"); + + b.Property("DesignId") + .HasColumnType("uuid") + .HasColumnName("design_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id"); + + b.HasIndex("DesignId"); + + b.ToTable("variables"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Vlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("InUse") + .HasColumnType("boolean") + .HasColumnName("in_use"); + + b.Property("PartitionId") + .HasColumnType("uuid") + .HasColumnName("partition_id"); + + b.Property("PoolId") + .HasColumnType("uuid") + .HasColumnName("pool_id"); + + b.Property("Reserved") + .HasColumnType("boolean") + .HasColumnName("reserved"); + + b.Property("Tag") + .HasColumnType("text") + .HasColumnName("tag"); + + b.Property("VlanId") + .HasColumnType("integer") + .HasColumnName("vlan_id"); + + b.HasKey("Id"); + + b.HasIndex("PartitionId"); + + b.HasIndex("PoolId"); + + b.HasIndex("VlanId"); + + b.ToTable("vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AzureDestroyFailureThreshold") + .HasColumnType("integer") + .HasColumnName("azure_destroy_failure_threshold"); + + b.Property("DirectoryId") + .HasColumnType("uuid") + .HasColumnName("directory_id"); + + b.Property("DynamicHost") + .HasColumnType("boolean") + .HasColumnName("dynamic_host"); + + b.Property("HostId") + .HasColumnType("uuid") + .HasColumnName("host_id"); + + b.Property("LastSynced") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_synced"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Parallelism") + .HasColumnType("integer") + .HasColumnName("parallelism"); + + b.Property("State") + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("StateBackup") + .HasColumnType("text") + .HasColumnName("state_backup"); + + b.Property("SyncErrors") + .HasColumnType("text") + .HasColumnName("sync_errors"); + + b.Property("TerraformVersion") + .HasColumnType("text") + .HasColumnName("terraform_version"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryId"); + + b.HasIndex("HostId"); + + b.ToTable("workspaces"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Apply", b => + { + b.HasOne("Caster.Api.Domain.Models.Run", "Run") + .WithOne("Apply") + .HasForeignKey("Caster.Api.Domain.Models.Apply", "RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Designs") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Directory"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.DesignModule", b => + { + b.HasOne("Caster.Api.Domain.Models.Design", "Design") + .WithMany("Modules") + .HasForeignKey("DesignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Module", "Module") + .WithMany() + .HasForeignKey("ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Design"); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany("Directories") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Parent"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Files") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "LockedBy") + .WithMany() + .HasForeignKey("LockedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany("Files") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Directory"); + + b.Navigation("LockedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.FileVersion", b => + { + b.HasOne("Caster.Api.Domain.Models.File", "File") + .WithMany("FileVersions") + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "TaggedBy") + .WithMany() + .HasForeignKey("TaggedById"); + + b.Navigation("File"); + + b.Navigation("ModifiedBy"); + + b.Navigation("TaggedBy"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.GroupMembership", b => + { + b.HasOne("Caster.Api.Domain.Models.Group", "Group") + .WithMany("Memberships") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "User") + .WithMany("GroupMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.HostMachine", b => + { + b.HasOne("Caster.Api.Domain.Models.Host", "Host") + .WithMany("Machines") + .HasForeignKey("HostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Host"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ModuleVersion", b => + { + b.HasOne("Caster.Api.Domain.Models.Module", "Module") + .WithMany("Versions") + .HasForeignKey("ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.HasOne("Caster.Api.Domain.Models.Pool", "Pool") + .WithMany("Partitions") + .HasForeignKey("PoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Pool"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Plan", b => + { + b.HasOne("Caster.Api.Domain.Models.Run", "Run") + .WithOne("Plan") + .HasForeignKey("Caster.Api.Domain.Models.Plan", "RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.HasOne("Caster.Api.Domain.Models.Partition", "Partition") + .WithMany() + .HasForeignKey("PartitionId"); + + b.Navigation("Partition"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectMembership", b => + { + b.HasOne("Caster.Api.Domain.Models.Group", "Group") + .WithMany("ProjectMemberships") + .HasForeignKey("GroupId"); + + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany("Memberships") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.ProjectRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "User") + .WithMany("ProjectMemberships") + .HasForeignKey("UserId"); + + b.Navigation("Group"); + + b.Navigation("Project"); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.HasOne("Caster.Api.Domain.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("Caster.Api.Domain.Models.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById"); + + b.HasOne("Caster.Api.Domain.Models.Workspace", "Workspace") + .WithMany("Runs") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.HasOne("Caster.Api.Domain.Models.SystemRole", "Role") + .WithMany() + .HasForeignKey("RoleId"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Variable", b => + { + b.HasOne("Caster.Api.Domain.Models.Design", "Design") + .WithMany("Variables") + .HasForeignKey("DesignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Design"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Vlan", b => + { + b.HasOne("Caster.Api.Domain.Models.Partition", null) + .WithMany("Vlans") + .HasForeignKey("PartitionId"); + + b.HasOne("Caster.Api.Domain.Models.Pool", null) + .WithMany("Vlans") + .HasForeignKey("PoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.HasOne("Caster.Api.Domain.Models.Directory", "Directory") + .WithMany("Workspaces") + .HasForeignKey("DirectoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.Host", "Host") + .WithMany() + .HasForeignKey("HostId"); + + b.Navigation("Directory"); + + b.Navigation("Host"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Design", b => + { + b.Navigation("Modules"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Directory", b => + { + b.Navigation("Children"); + + b.Navigation("Designs"); + + b.Navigation("Files"); + + b.Navigation("Workspaces"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.File", b => + { + b.Navigation("FileVersions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Group", b => + { + b.Navigation("Memberships"); + + b.Navigation("ProjectMemberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => + { + b.Navigation("Machines"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Module", b => + { + b.Navigation("Versions"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Partition", b => + { + b.Navigation("Vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Pool", b => + { + b.Navigation("Partitions"); + + b.Navigation("Vlans"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => + { + b.Navigation("Directories"); + + b.Navigation("Memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => + { + b.Navigation("Apply"); + + b.Navigation("Plan"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + { + b.Navigation("GroupMemberships"); + + b.Navigation("ProjectMemberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => + { + b.Navigation("Files"); + + b.Navigation("Runs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.cs b/src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.cs new file mode 100644 index 0000000..a7cd574 --- /dev/null +++ b/src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Caster.Api.Data.Migrations +{ + /// + public partial class Add_Import_Resources : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "system_roles", + keyColumn: "id", + keyValue: new Guid("1da3027e-725d-4753-9455-a836ed9bdb1e"), + column: "permissions", + value: new[] { 1, 7, 9, 11, 13, 15, 17, 19 }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "system_roles", + keyColumn: "id", + keyValue: new Guid("1da3027e-725d-4753-9455-a836ed9bdb1e"), + column: "permissions", + value: new[] { 1, 6, 8, 10, 12, 14, 16, 18 }); + } + } +} diff --git a/src/Caster.Api/Data/Migrations/CasterContextModelSnapshot.cs b/src/Caster.Api/Data/Migrations/CasterContextModelSnapshot.cs index 6f122fe..91f1715 100644 --- a/src/Caster.Api/Data/Migrations/CasterContextModelSnapshot.cs +++ b/src/Caster.Api/Data/Migrations/CasterContextModelSnapshot.cs @@ -1,8 +1,3 @@ -/* -Copyright 2021 Carnegie Mellon University. All Rights Reserved. - Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. -*/ - // using System; using Caster.Api.Data; @@ -285,6 +280,56 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("file_versions"); }); + modelBuilder.Entity("Caster.Api.Domain.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("groups"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("GroupId", "UserId") + .IsUnique(); + + b.ToTable("group_memberships"); + }); + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => { b.Property("Id") @@ -448,38 +493,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("partitions"); }); - modelBuilder.Entity("Caster.Api.Domain.Models.Permission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id") - .HasDefaultValueSql("uuid_generate_v4()"); - - b.Property("Description") - .HasColumnType("text") - .HasColumnName("description"); - - b.Property("Key") - .HasColumnType("text") - .HasColumnName("key"); - - b.Property("ReadOnly") - .HasColumnType("boolean") - .HasColumnName("read_only"); - - b.Property("Value") - .HasColumnType("text") - .HasColumnName("value"); - - b.HasKey("Id"); - - b.HasIndex("Key", "Value") - .IsUnique(); - - b.ToTable("permissions"); - }); - modelBuilder.Entity("Caster.Api.Domain.Models.Plan", b => { b.Property("Id") @@ -552,6 +565,101 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("projects"); }); + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("ProjectId") + .HasColumnType("uuid") + .HasColumnName("project_id"); + + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValue(new Guid("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4")) + .HasColumnName("role_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId", "GroupId") + .IsUnique(); + + b.ToTable("project_memberships"); + }); + + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id") + .HasDefaultValueSql("uuid_generate_v4()"); + + b.Property("AllPermissions") + .HasColumnType("boolean") + .HasColumnName("all_permissions"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("integer[]") + .HasColumnName("permissions"); + + b.HasKey("Id"); + + b.ToTable("project_roles"); + + b.HasData( + new + { + Id = new Guid("1a3f26cd-9d99-4b98-b914-12931e786198"), + AllPermissions = true, + Description = "Can perform all actions on the Project", + Name = "Manager", + Permissions = new int[0] + }, + new + { + Id = new Guid("39aa296e-05ba-4fb0-8d74-c92cf3354c6f"), + AllPermissions = false, + Description = "Has read only access to the Project", + Name = "Observer", + Permissions = new[] { 0 } + }, + new + { + Id = new Guid("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4"), + AllPermissions = false, + Description = "Has read only access to the Project", + Name = "Member", + Permissions = new[] { 0, 1, 3 } + }); + }); + modelBuilder.Entity("Caster.Api.Domain.Models.RemovedResource", b => { b.Property("Id") @@ -620,7 +728,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("runs"); }); - modelBuilder.Entity("Caster.Api.Domain.Models.User", b => + modelBuilder.Entity("Caster.Api.Domain.Models.SystemRole", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -628,16 +736,64 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnName("id") .HasDefaultValueSql("uuid_generate_v4()"); + b.Property("AllPermissions") + .HasColumnType("boolean") + .HasColumnName("all_permissions"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Immutable") + .HasColumnType("boolean") + .HasColumnName("immutable"); + b.Property("Name") .HasColumnType("text") .HasColumnName("name"); + b.Property("Permissions") + .HasColumnType("integer[]") + .HasColumnName("permissions"); + b.HasKey("Id"); - b.ToTable("users"); + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("system_roles"); + + b.HasData( + new + { + Id = new Guid("f35e8fff-f996-4cba-b303-3ba515ad8d2f"), + AllPermissions = true, + Description = "Can perform all actions.", + Immutable = true, + Name = "Administrator", + Permissions = new int[0] + }, + new + { + Id = new Guid("d80b73c3-95d7-4468-8650-c62bbd082507"), + AllPermissions = false, + Description = "Can create and manage their own Projects.", + Immutable = false, + Name = "Content Developer", + Permissions = new[] { 0 } + }, + new + { + Id = new Guid("1da3027e-725d-4753-9455-a836ed9bdb1e"), + AllPermissions = false, + Description = "Can perform all View actions, but not make any changes.", + Immutable = false, + Name = "Observer", + Permissions = new[] { 1, 7, 9, 11, 13, 15, 17, 19 } + }); }); - modelBuilder.Entity("Caster.Api.Domain.Models.UserPermission", b => + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -645,22 +801,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnName("id") .HasDefaultValueSql("uuid_generate_v4()"); - b.Property("PermissionId") - .HasColumnType("uuid") - .HasColumnName("permission_id"); + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); - b.Property("UserId") + b.Property("RoleId") .HasColumnType("uuid") - .HasColumnName("user_id"); + .HasColumnName("role_id"); b.HasKey("Id"); - b.HasIndex("PermissionId"); + b.HasIndex("RoleId"); - b.HasIndex("UserId", "PermissionId") - .IsUnique(); - - b.ToTable("user_permissions"); + b.ToTable("users"); }); modelBuilder.Entity("Caster.Api.Domain.Models.Variable", b => @@ -910,6 +1063,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("TaggedBy"); }); + modelBuilder.Entity("Caster.Api.Domain.Models.GroupMembership", b => + { + b.HasOne("Caster.Api.Domain.Models.Group", "Group") + .WithMany("Memberships") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "User") + .WithMany("GroupMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => { b.HasOne("Caster.Api.Domain.Models.Project", "Project") @@ -980,6 +1152,37 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Partition"); }); + modelBuilder.Entity("Caster.Api.Domain.Models.ProjectMembership", b => + { + b.HasOne("Caster.Api.Domain.Models.Group", "Group") + .WithMany("ProjectMemberships") + .HasForeignKey("GroupId"); + + b.HasOne("Caster.Api.Domain.Models.Project", "Project") + .WithMany("Memberships") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.ProjectRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Caster.Api.Domain.Models.User", "User") + .WithMany("ProjectMemberships") + .HasForeignKey("UserId"); + + b.Navigation("Group"); + + b.Navigation("Project"); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => { b.HasOne("Caster.Api.Domain.Models.User", "CreatedBy") @@ -1003,23 +1206,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Workspace"); }); - modelBuilder.Entity("Caster.Api.Domain.Models.UserPermission", b => + modelBuilder.Entity("Caster.Api.Domain.Models.User", b => { - b.HasOne("Caster.Api.Domain.Models.Permission", "Permission") - .WithMany("UserPermissions") - .HasForeignKey("PermissionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Caster.Api.Domain.Models.User", "User") - .WithMany("UserPermissions") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Permission"); + b.HasOne("Caster.Api.Domain.Models.SystemRole", "Role") + .WithMany() + .HasForeignKey("RoleId"); - b.Navigation("User"); + b.Navigation("Role"); }); modelBuilder.Entity("Caster.Api.Domain.Models.Variable", b => @@ -1086,6 +1279,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("FileVersions"); }); + modelBuilder.Entity("Caster.Api.Domain.Models.Group", b => + { + b.Navigation("Memberships"); + + b.Navigation("ProjectMemberships"); + }); + modelBuilder.Entity("Caster.Api.Domain.Models.Host", b => { b.Navigation("Machines"); @@ -1101,11 +1301,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Vlans"); }); - modelBuilder.Entity("Caster.Api.Domain.Models.Permission", b => - { - b.Navigation("UserPermissions"); - }); - modelBuilder.Entity("Caster.Api.Domain.Models.Pool", b => { b.Navigation("Partitions"); @@ -1116,6 +1311,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Caster.Api.Domain.Models.Project", b => { b.Navigation("Directories"); + + b.Navigation("Memberships"); }); modelBuilder.Entity("Caster.Api.Domain.Models.Run", b => @@ -1127,7 +1324,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Caster.Api.Domain.Models.User", b => { - b.Navigation("UserPermissions"); + b.Navigation("GroupMemberships"); + + b.Navigation("ProjectMemberships"); }); modelBuilder.Entity("Caster.Api.Domain.Models.Workspace", b => diff --git a/src/Caster.Api/Domain/Models/Apply.cs b/src/Caster.Api/Domain/Models/Apply.cs index fc426f4..af8191f 100644 --- a/src/Caster.Api/Domain/Models/Apply.cs +++ b/src/Caster.Api/Domain/Models/Apply.cs @@ -8,7 +8,7 @@ namespace Caster.Api.Domain.Models { - public class Apply + public class Apply : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/src/Caster.Api/Domain/Models/Directory.cs b/src/Caster.Api/Domain/Models/Directory.cs index c473620..f495777 100644 --- a/src/Caster.Api/Domain/Models/Directory.cs +++ b/src/Caster.Api/Domain/Models/Directory.cs @@ -12,7 +12,7 @@ namespace Caster.Api.Domain.Models { - public class Directory + public class Directory : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/src/Caster.Api/Domain/Models/File.cs b/src/Caster.Api/Domain/Models/File.cs index c12a62c..1cab4da 100644 --- a/src/Caster.Api/Domain/Models/File.cs +++ b/src/Caster.Api/Domain/Models/File.cs @@ -11,7 +11,7 @@ namespace Caster.Api.Domain.Models { - public class File + public class File : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] @@ -19,7 +19,7 @@ public class File public string Name { get; set; } - public Guid DirectoryId { get; private set;} + public Guid DirectoryId { get; private set; } public virtual Directory Directory { get; set; } public Guid? WorkspaceId { get; private set; } @@ -31,7 +31,7 @@ public class File public Guid? LockedById { get; private set; } public virtual User LockedBy { get; private set; } - public string Content { get; set;} + public string Content { get; set; } public DateTime? DateSaved { get; private set; } @@ -62,11 +62,11 @@ public void Tag(string tag, Guid userId, DateTime dateTagged) this.FileVersions.Add(fileVersion); } - public void Save(Guid userId, bool isAdmin, bool bypassLock = false) + public void Save(Guid userId, bool canLock, bool bypassLock = false) { if (!bypassLock) { - if (this.AdministrativelyLocked && !isAdmin) + if (this.AdministrativelyLocked && !canLock) throw new FileAdminLockedException(); this.VerifyLock(userId); @@ -78,17 +78,17 @@ public void Save(Guid userId, bool isAdmin, bool bypassLock = false) this.FileVersions.Add(new FileVersion(this)); } - public void Delete(bool isAdmin) + public void Delete(bool canLock) { - if (this.AdministrativelyLocked && !isAdmin) + if (this.AdministrativelyLocked && !canLock) throw new FileAdminLockedException(); this.IsDeleted = true; } - public void Lock(Guid userId, bool isAdmin) + public void Lock(Guid userId, bool canLock) { - if (this.AdministrativelyLocked && !isAdmin) + if (this.AdministrativelyLocked && !canLock) throw new FileAdminLockedException(); this.VerifyLock(userId, invalidOnUnlocked: false); @@ -101,23 +101,23 @@ public void Unlock(Guid userId) this.LockedById = null; } - public void AdministrativelyLock(bool isAdmin) + public void AdministrativelyLock(bool canLock) { - if (!isAdmin) + if (!canLock) throw new FileInsufficientPrivilegesException(); this.AdministrativelyLocked = true; } - public void AdministrativelyUnlock(bool isAdmin) + public void AdministrativelyUnlock(bool canLock) { - if (!isAdmin) + if (!canLock) throw new FileInsufficientPrivilegesException(); this.AdministrativelyLocked = false; } - public bool CanLock(Guid userId, bool isAdmin) + public bool CanLock(Guid userId, bool canLock) { if (this.LockedById.HasValue) { @@ -131,7 +131,7 @@ public bool CanLock(Guid userId, bool isAdmin) } } - if (this.AdministrativelyLocked && !isAdmin) + if (this.AdministrativelyLocked && !canLock) { return false; } diff --git a/src/Caster.Api/Domain/Models/FileVersion.cs b/src/Caster.Api/Domain/Models/FileVersion.cs index 852cd20..f6d9856 100644 --- a/src/Caster.Api/Domain/Models/FileVersion.cs +++ b/src/Caster.Api/Domain/Models/FileVersion.cs @@ -9,7 +9,7 @@ namespace Caster.Api.Domain.Models { - public class FileVersion + public class FileVersion : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] @@ -23,7 +23,7 @@ public class FileVersion public Guid? ModifiedById { get; set; } public virtual User ModifiedBy { get; set; } - public string Content { get; set;} + public string Content { get; set; } public DateTime? DateSaved { get; set; } public string Tag { get; set; } @@ -32,7 +32,7 @@ public class FileVersion public DateTime? DateTagged { get; set; } - public FileVersion() {} + public FileVersion() { } public FileVersion(File file) { diff --git a/src/Caster.Api/Domain/Models/Group.cs b/src/Caster.Api/Domain/Models/Group.cs new file mode 100644 index 0000000..92b1235 --- /dev/null +++ b/src/Caster.Api/Domain/Models/Group.cs @@ -0,0 +1,32 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Caster.Api.Domain.Models; + +public class Group : IEntity +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } + + public string Name { get; set; } + public string Description { get; set; } + + public virtual ICollection Memberships { get; set; } = new List(); + public virtual ICollection ProjectMemberships { get; set; } = new List(); +} + +public class GroupConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasIndex(e => e.Name).IsUnique(); + } +} \ No newline at end of file diff --git a/src/Caster.Api/Domain/Models/GroupMembership.cs b/src/Caster.Api/Domain/Models/GroupMembership.cs new file mode 100644 index 0000000..f249f2e --- /dev/null +++ b/src/Caster.Api/Domain/Models/GroupMembership.cs @@ -0,0 +1,50 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Caster.Api.Domain.Models; + +public class GroupMembership : IEntity +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } + + public Guid GroupId { get; set; } + public virtual Group Group { get; set; } + + public Guid UserId { get; set; } + public virtual User User { get; set; } + + public GroupMembership() { } + + public GroupMembership(Guid groupId, Guid userId) + { + GroupId = groupId; + UserId = userId; + } + + public class GroupMembershipConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasIndex(e => new { e.GroupId, e.UserId }).IsUnique(); + + builder + .HasOne(tu => tu.Group) + .WithMany(t => t.Memberships) + .HasForeignKey(tu => tu.GroupId); + + builder + .HasOne(tu => tu.User) + .WithMany(u => u.GroupMemberships) + .HasForeignKey(tu => tu.UserId) + .HasPrincipalKey(u => u.Id); + } + } +} diff --git a/src/Caster.Api/Domain/Models/Host.cs b/src/Caster.Api/Domain/Models/Host.cs index 47d2d2e..e937a9d 100644 --- a/src/Caster.Api/Domain/Models/Host.cs +++ b/src/Caster.Api/Domain/Models/Host.cs @@ -8,7 +8,7 @@ namespace Caster.Api.Domain.Models { - public class Host + public class Host : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/src/Caster.Api/Domain/Models/IEntity.cs b/src/Caster.Api/Domain/Models/IEntity.cs new file mode 100644 index 0000000..cde34de --- /dev/null +++ b/src/Caster.Api/Domain/Models/IEntity.cs @@ -0,0 +1,3 @@ +namespace Caster.Api.Domain.Models; + +public interface IEntity { } \ No newline at end of file diff --git a/src/Caster.Api/Domain/Models/Modules/Design.cs b/src/Caster.Api/Domain/Models/Modules/Design.cs index 2a331b1..176ff7c 100644 --- a/src/Caster.Api/Domain/Models/Modules/Design.cs +++ b/src/Caster.Api/Domain/Models/Modules/Design.cs @@ -13,7 +13,7 @@ namespace Caster.Api.Domain.Models; -public class Design +public class Design : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] @@ -32,7 +32,7 @@ public class ModuleValue public string Value { get; set; } } -public class DesignModule +public class DesignModule : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/src/Caster.Api/Domain/Models/Modules/Module.cs b/src/Caster.Api/Domain/Models/Modules/Module.cs index 66f69b6..d7ee599 100644 --- a/src/Caster.Api/Domain/Models/Modules/Module.cs +++ b/src/Caster.Api/Domain/Models/Modules/Module.cs @@ -8,7 +8,7 @@ namespace Caster.Api.Domain.Models; -public class Module +public class Module : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/src/Caster.Api/Domain/Models/Modules/ModuleOutput.cs b/src/Caster.Api/Domain/Models/Modules/ModuleOutput.cs index 7443a34..b939695 100644 --- a/src/Caster.Api/Domain/Models/Modules/ModuleOutput.cs +++ b/src/Caster.Api/Domain/Models/Modules/ModuleOutput.cs @@ -1,17 +1,6 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; -using Caster.Api.Infrastructure.Serialization; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - namespace Caster.Api.Domain.Models; public class ModuleOutput diff --git a/src/Caster.Api/Domain/Models/Modules/ModuleVersion.cs b/src/Caster.Api/Domain/Models/Modules/ModuleVersion.cs index 14ba1af..f89b0a7 100644 --- a/src/Caster.Api/Domain/Models/Modules/ModuleVersion.cs +++ b/src/Caster.Api/Domain/Models/Modules/ModuleVersion.cs @@ -14,7 +14,7 @@ namespace Caster.Api.Domain.Models; -public class ModuleVersion +public class ModuleVersion : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/src/Caster.Api/Domain/Models/Modules/Variable.cs b/src/Caster.Api/Domain/Models/Modules/Variable.cs index 050a531..3bc17bd 100644 --- a/src/Caster.Api/Domain/Models/Modules/Variable.cs +++ b/src/Caster.Api/Domain/Models/Modules/Variable.cs @@ -13,7 +13,7 @@ namespace Caster.Api.Domain.Models; -public class Variable +public class Variable : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/src/Caster.Api/Domain/Models/Partition.cs b/src/Caster.Api/Domain/Models/Partition.cs index 3099b8e..500ef17 100644 --- a/src/Caster.Api/Domain/Models/Partition.cs +++ b/src/Caster.Api/Domain/Models/Partition.cs @@ -8,7 +8,7 @@ namespace Caster.Api.Domain.Models { - public class Partition + public class Partition : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/src/Caster.Api/Domain/Models/Permission.cs b/src/Caster.Api/Domain/Models/Permission.cs deleted file mode 100644 index 8a7aaae..0000000 --- a/src/Caster.Api/Domain/Models/Permission.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Caster.Api.Domain.Models -{ - public class Permission - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public Guid Id { get; set; } - - public string Key { get; set; } - - public string Value { get; set; } - - public string Description { get; set; } - - public bool ReadOnly { get; set; } - public ICollection UserPermissions { get; set; } = new List(); - } - - public class PermissionConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) - { - builder.HasIndex(x => new { x.Key, x.Value }).IsUnique(); - } - } -} - diff --git a/src/Caster.Api/Domain/Models/Plan.cs b/src/Caster.Api/Domain/Models/Plan.cs index f77ff44..588fca4 100644 --- a/src/Caster.Api/Domain/Models/Plan.cs +++ b/src/Caster.Api/Domain/Models/Plan.cs @@ -10,7 +10,7 @@ namespace Caster.Api.Domain.Models { - public class Plan + public class Plan : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] @@ -23,13 +23,13 @@ public class Plan public string Output { get; set; } } - public enum PlanStatus + public enum PlanStatus { Queued = 0, Failed = 1, Rejected = 2, Planning = 3, - Planned = 4 + Planned = 4 } } diff --git a/src/Caster.Api/Domain/Models/Pool.cs b/src/Caster.Api/Domain/Models/Pool.cs index 0b8040f..77c84df 100644 --- a/src/Caster.Api/Domain/Models/Pool.cs +++ b/src/Caster.Api/Domain/Models/Pool.cs @@ -8,7 +8,7 @@ namespace Caster.Api.Domain.Models { - public class Pool + public class Pool : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/src/Caster.Api/Domain/Models/Project.cs b/src/Caster.Api/Domain/Models/Project.cs index 83dec74..9a1ae52 100644 --- a/src/Caster.Api/Domain/Models/Project.cs +++ b/src/Caster.Api/Domain/Models/Project.cs @@ -8,7 +8,7 @@ namespace Caster.Api.Domain.Models { - public class Project + public class Project : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] @@ -21,6 +21,8 @@ public class Project public Guid? PartitionId { get; set; } public virtual Partition Partition { get; set; } + public virtual ICollection Memberships { get; set; } = new List(); + public Project() { } public Project(string name) diff --git a/src/Caster.Api/Domain/Models/ProjectMembership.cs b/src/Caster.Api/Domain/Models/ProjectMembership.cs new file mode 100644 index 0000000..99719cb --- /dev/null +++ b/src/Caster.Api/Domain/Models/ProjectMembership.cs @@ -0,0 +1,67 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Caster.Api.Domain.Models; + +public class ProjectMembership : IEntity +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } + + public Guid ProjectId { get; set; } + public virtual Project Project { get; set; } + + public Guid? UserId { get; set; } + public virtual User User { get; set; } + + public Guid? GroupId { get; set; } + public virtual Group Group { get; set; } + + public Guid RoleId { get; set; } = ProjectRoleDefaults.ProjectMemberRoleId; + public ProjectRole Role { get; set; } + + + public ProjectMembership() { } + + public ProjectMembership(Guid projectId, Guid? userId, Guid? groupId) + { + ProjectId = projectId; + UserId = userId; + GroupId = groupId; + } + + public class ProjectMembershipConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasIndex(e => new { e.ProjectId, e.UserId, e.GroupId }).IsUnique(); + + builder.Property(x => x.RoleId).HasDefaultValue(ProjectRoleDefaults.ProjectMemberRoleId); + + builder + .HasOne(x => x.Project) + .WithMany(x => x.Memberships) + .HasForeignKey(x => x.ProjectId); + + builder + .HasOne(x => x.User) + .WithMany(x => x.ProjectMemberships) + .HasForeignKey(x => x.UserId) + .HasPrincipalKey(x => x.Id); + + builder + .HasOne(x => x.Group) + .WithMany(x => x.ProjectMemberships) + .HasForeignKey(x => x.GroupId) + .HasPrincipalKey(x => x.Id); + } + } +} diff --git a/src/Caster.Api/Domain/Models/ProjectRole.cs b/src/Caster.Api/Domain/Models/ProjectRole.cs new file mode 100644 index 0000000..7e37a32 --- /dev/null +++ b/src/Caster.Api/Domain/Models/ProjectRole.cs @@ -0,0 +1,80 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Caster.Api.Domain.Models; + +public class ProjectRole : IEntity +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public bool AllPermissions { get; set; } + + public List Permissions { get; set; } +} + +public enum ProjectPermission +{ + ViewProject, + EditProject, + ManageProject, + ImportProject, + LockFiles + +} + +public static class ProjectRoleDefaults +{ + public static Guid ProjectCreatorRoleId = new("1a3f26cd-9d99-4b98-b914-12931e786198"); + public static Guid ProjectReadOnlyRoleId = new("39aa296e-05ba-4fb0-8d74-c92cf3354c6f"); + public static Guid ProjectMemberRoleId = new("f870d8ee-7332-4f7f-8ee0-63bd07cfd7e4"); +} + +public class ProjectRoleConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasData( + new ProjectRole + { + Id = ProjectRoleDefaults.ProjectCreatorRoleId, + Name = "Manager", + AllPermissions = true, + Permissions = [], + Description = "Can perform all actions on the Project" + }, + new ProjectRole + { + Id = ProjectRoleDefaults.ProjectReadOnlyRoleId, + Name = "Observer", + AllPermissions = false, + Permissions = [ProjectPermission.ViewProject], + Description = "Has read only access to the Project" + }, + new ProjectRole + { + Id = ProjectRoleDefaults.ProjectMemberRoleId, + Name = "Member", + AllPermissions = false, + Permissions = [ + ProjectPermission.ViewProject, + ProjectPermission.EditProject, + ProjectPermission.ImportProject + ], + Description = "Has read only access to the Project" + } + ); + } +} \ No newline at end of file diff --git a/src/Caster.Api/Domain/Models/RemovedResource.cs b/src/Caster.Api/Domain/Models/RemovedResource.cs index 41e5509..e283d9c 100644 --- a/src/Caster.Api/Domain/Models/RemovedResource.cs +++ b/src/Caster.Api/Domain/Models/RemovedResource.cs @@ -8,7 +8,7 @@ namespace Caster.Api.Domain.Models { - public class RemovedResource + public class RemovedResource : IEntity { [Key] public string Id { get; set; } diff --git a/src/Caster.Api/Domain/Models/Run.cs b/src/Caster.Api/Domain/Models/Run.cs index cf0e9da..95f372d 100644 --- a/src/Caster.Api/Domain/Models/Run.cs +++ b/src/Caster.Api/Domain/Models/Run.cs @@ -13,7 +13,7 @@ namespace Caster.Api.Domain.Models { - public class Run + public class Run : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/src/Caster.Api/Domain/Models/SystemRole.cs b/src/Caster.Api/Domain/Models/SystemRole.cs new file mode 100644 index 0000000..e2d9086 --- /dev/null +++ b/src/Caster.Api/Domain/Models/SystemRole.cs @@ -0,0 +1,99 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Caster.Api.Domain.Models; + +public class SystemRole : IEntity +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public bool AllPermissions { get; set; } + public bool Immutable { get; set; } + + public List Permissions { get; set; } +} + +public enum SystemPermission +{ + CreateProjects, + ViewProjects, + EditProjects, + ManageProjects, + ImportProjects, + LockFiles, + ImportResources, + ViewUsers, + ManageUsers, + ViewWorkspaces, + ManageWorkspaces, + ViewVLANs, + ManageVLANs, + ViewRoles, + ManageRoles, + ViewGroups, + ManageGroups, + ViewHosts, + ManageHosts, + ViewModules, + ManageModules +} + +public static class SystemRoleDefaults +{ + public static Guid AdministratorRoleId = new("f35e8fff-f996-4cba-b303-3ba515ad8d2f"); + public static Guid ContentDeveloperRoleId = new("d80b73c3-95d7-4468-8650-c62bbd082507"); + public static Guid ObserverRoleId = new("1da3027e-725d-4753-9455-a836ed9bdb1e"); +} + +public class SystemRoleConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasIndex(x => x.Name).IsUnique(); + + builder.HasData( + new SystemRole + { + Id = SystemRoleDefaults.AdministratorRoleId, + Name = "Administrator", + AllPermissions = true, + Immutable = true, + Permissions = [], + Description = "Can perform all actions." + }, + new SystemRole + { + Id = SystemRoleDefaults.ContentDeveloperRoleId, + Name = "Content Developer", + AllPermissions = false, + Immutable = false, + Permissions = [ + SystemPermission.CreateProjects + ], + Description = "Can create and manage their own Projects." + }, + new SystemRole + { + Id = SystemRoleDefaults.ObserverRoleId, + Name = "Observer", + AllPermissions = false, + Immutable = false, + Permissions = Enum.GetValues() + .Where(x => x.ToString().StartsWith("View")) + .ToList(), + Description = "Can perform all View actions, but not make any changes." + } + ); + } +} \ No newline at end of file diff --git a/src/Caster.Api/Domain/Models/User.cs b/src/Caster.Api/Domain/Models/User.cs index 2f6b291..b2e97f7 100644 --- a/src/Caster.Api/Domain/Models/User.cs +++ b/src/Caster.Api/Domain/Models/User.cs @@ -10,12 +10,17 @@ namespace Caster.Api.Domain.Models { - public class User + public class User : IEntity { [Key] public Guid Id { get; set; } public string Name { get; set; } - public ICollection UserPermissions { get; set; } = new List(); + + public Guid? RoleId { get; set; } + public virtual SystemRole Role { get; set; } + + public ICollection ProjectMemberships { get; set; } = new List(); + public ICollection GroupMemberships { get; set; } = new List(); } } diff --git a/src/Caster.Api/Domain/Models/UserPermission.cs b/src/Caster.Api/Domain/Models/UserPermission.cs deleted file mode 100644 index 2ec5c5b..0000000 --- a/src/Caster.Api/Domain/Models/UserPermission.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Caster.Api.Domain.Models -{ - public class UserPermission - { - public UserPermission() { } - - public UserPermission(Guid userId, Guid permissionId) - { - UserId = userId; - PermissionId = permissionId; - } - - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public Guid Id { get; set; } - - public Guid UserId { get; set; } - public User User { get; set; } - - public Guid PermissionId { get; set; } - public Permission Permission { get; set; } - } - - public class UserPermissionConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) - { - builder.HasIndex(x => new { x.UserId, x.PermissionId }).IsUnique(); - - builder - .HasOne(u => u.User) - .WithMany(p => p.UserPermissions) - .HasForeignKey(x => x.UserId); - builder - .HasOne(u => u.Permission) - .WithMany(p => p.UserPermissions) - .HasForeignKey(x => x.PermissionId); - } - } -} - diff --git a/src/Caster.Api/Domain/Models/Vlan.cs b/src/Caster.Api/Domain/Models/Vlan.cs index b4506e0..740db64 100644 --- a/src/Caster.Api/Domain/Models/Vlan.cs +++ b/src/Caster.Api/Domain/Models/Vlan.cs @@ -9,7 +9,7 @@ namespace Caster.Api.Domain.Models { - public class Vlan + public class Vlan : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/src/Caster.Api/Domain/Models/Workspace.cs b/src/Caster.Api/Domain/Models/Workspace.cs index 5265433..e35aae0 100644 --- a/src/Caster.Api/Domain/Models/Workspace.cs +++ b/src/Caster.Api/Domain/Models/Workspace.cs @@ -15,7 +15,7 @@ namespace Caster.Api.Domain.Models { - public class Workspace + public class Workspace : IEntity { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/src/Caster.Api/Domain/Services/UserClaimsService.cs b/src/Caster.Api/Domain/Services/UserClaimsService.cs index 5832fd2..0c92681 100644 --- a/src/Caster.Api/Domain/Services/UserClaimsService.cs +++ b/src/Caster.Api/Domain/Services/UserClaimsService.cs @@ -14,6 +14,9 @@ using Caster.Api.Infrastructure.Extensions; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Options; +using System.Text.Json; +using Microsoft.IdentityModel.JsonWebTokens; +using System.Text.RegularExpressions; namespace Caster.Api.Domain.Services { @@ -43,17 +46,36 @@ public UserClaimsService(CasterContext context, IMemoryCache cache, ClaimsTransf public async Task AddUserClaims(ClaimsPrincipal principal, bool update) { List claims; - var identity = ((ClaimsIdentity)principal.Identity); + var identity = (ClaimsIdentity)principal.Identity; var userId = principal.GetId(); - if (!_cache.TryGetValue(userId, out claims)) + // Don't use cached claims if given a new token and we are using roles or groups from the token + if (_cache.TryGetValue(userId, out claims) && (_options.UseGroupsFromIdP || _options.UseRolesFromIdP)) { - claims = new List(); + var cachedTokenId = claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Jti)?.Value; + var newTokenId = identity.Claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Jti)?.Value; + + if (newTokenId != cachedTokenId) + { + claims = null; + } + } + + if (claims == null) + { + claims = []; var user = await ValidateUser(userId, principal.FindFirst("name")?.Value, update); if (user != null) { - claims.AddRange(await GetUserClaims(userId)); + var jtiClaim = identity.Claims.Where(x => x.Type == JwtRegisteredClaimNames.Jti).FirstOrDefault(); + + if (jtiClaim is not null) + { + claims.Add(new Claim(jtiClaim.Type, jtiClaim.Value)); + } + + claims.AddRange(await GetPermissionClaims(userId, principal)); if (_options.EnableCaching) { @@ -61,6 +83,7 @@ public async Task AddUserClaims(ClaimsPrincipal principal, bool } } } + addNewClaims(identity, claims); return principal; } @@ -115,17 +138,6 @@ private async Task ValidateUser(Guid subClaim, string nameClaim, bool upda Name = nameClaim ?? "Anonymous" }; - // First user is default SystemAdmin - if (!anyUsers) - { - var systemAdminPermission = await _context.Permissions.Where(p => p.Key == nameof(CasterClaimTypes.SystemAdmin)).FirstOrDefaultAsync(); - - if (systemAdminPermission != null) - { - user.UserPermissions.Add(new UserPermission(user.Id, systemAdminPermission.Id)); - } - } - _context.Users.Add(user); await _context.SaveChangesAsync(); } @@ -143,36 +155,158 @@ private async Task ValidateUser(Guid subClaim, string nameClaim, bool upda return user; } - private async Task> GetUserClaims(Guid userId) + private async Task> GetPermissionClaims(Guid userId, ClaimsPrincipal principal) { - List claims = new List(); + List claims = new(); - var userPermissions = await _context.UserPermissions - .Where(u => u.UserId == userId) - .Include(x => x.Permission) - .ToArrayAsync(); + var tokenRoleNames = _options.UseRolesFromIdP ? + this.GetClaimsFromToken(principal, _options.RolesClaimPath).Select(x => x.ToLower()) : + []; - if (userPermissions.Where(x => x.Permission.Key == nameof(CasterClaimTypes.SystemAdmin)).Any()) + var roles = await _context.SystemRoles + .Where(x => tokenRoleNames.Contains(x.Name.ToLower())) + .ToListAsync(); + + var userRole = await _context.Users + .Where(x => x.Id == userId) + .Select(x => x.Role) + .FirstOrDefaultAsync(); + + if (userRole != null) { - claims.Add(new Claim(ClaimTypes.Role, nameof(CasterClaimTypes.SystemAdmin))); + roles.Add(userRole); } - if (userPermissions.Where(x => x.Permission.Key == nameof(CasterClaimTypes.ContentDeveloper)).Any()) + roles = roles.Distinct().ToList(); + + foreach (var role in roles) { - claims.Add(new Claim(ClaimTypes.Role, nameof(CasterClaimTypes.ContentDeveloper))); + List permissions; + + if (role.AllPermissions) + { + permissions = Enum.GetValues().Select(x => x.ToString()).ToList(); + } + else + { + permissions = role.Permissions.Select(x => x.ToString()).ToList(); + } + + foreach (var permission in permissions) + { + if (!claims.Any(x => x.Type == AuthorizationConstants.PermissionsClaimType && + x.Value == permission)) + { + claims.Add(new Claim(AuthorizationConstants.PermissionsClaimType, permission)); + }; + } } - if (userPermissions.Where(x => x.Permission.Key == nameof(CasterClaimTypes.Operator)).Any()) + var groupNames = _options.UseGroupsFromIdP ? + this.GetClaimsFromToken(principal, _options.GroupsClaimPath).Select(x => x.ToLower()) : + []; + + var groupIds = await _context.Groups + .Where(x => x.Memberships.Any(y => y.UserId == userId) || groupNames.Contains(x.Name.ToLower())) + .Select(x => x.Id) + .ToListAsync(); + + // Get Project Permissions + var projectMemberships = await _context.ProjectMemberships + .Where(x => x.UserId == userId || (x.GroupId.HasValue && groupIds.Contains(x.GroupId.Value))) + .Include(x => x.Role) + .GroupBy(x => x.ProjectId) + .ToListAsync(); + + foreach (var group in projectMemberships) { - claims.Add(new Claim(ClaimTypes.Role, nameof(CasterClaimTypes.Operator))); + var projectPermissions = new List(); + + foreach (var membership in group) + { + if (membership.Role.AllPermissions) + { + projectPermissions.AddRange(Enum.GetValues()); + } + else + { + projectPermissions.AddRange(membership.Role.Permissions); + } + } + + var permissionsClaim = new ProjectPermissionsClaim + { + ProjectId = group.Key, + Permissions = projectPermissions.Distinct().ToArray() + }; + + claims.Add(new Claim(AuthorizationConstants.ProjectPermissionsClaimType, permissionsClaim.ToString())); + } + + return claims; + } + + private string[] GetClaimsFromToken(ClaimsPrincipal principal, string claimPath) + { + if (string.IsNullOrEmpty(claimPath)) + { + return []; } - if (userPermissions.Where(x => x.Permission.Key == nameof(CasterClaimTypes.BaseUser)).Any()) + // Name of the claim to insert into the token. This can be a fully qualified name like 'address.street'. + // In this case, a nested json object will be created. To prevent nesting and use dot literally, escape the dot with backslash (\.). + var pathSegments = Regex.Split(claimPath, @"(? s.Replace("\\.", ".")).ToArray(); + + var tokenClaim = principal.Claims.Where(x => x.Type == pathSegments.First()).FirstOrDefault(); + + if (tokenClaim == null) { - claims.Add(new Claim(ClaimTypes.Role, nameof(CasterClaimTypes.BaseUser))); + return []; } - return claims; + return tokenClaim.ValueType switch + { + ClaimValueTypes.String => [tokenClaim.Value], + JsonClaimValueTypes.Json => ExtractJsonClaimValues(tokenClaim.Value, pathSegments.Skip(1)), + _ => [] + }; + } + + private string[] ExtractJsonClaimValues(string json, IEnumerable pathSegments) + { + List values = new(); + try + { + using JsonDocument doc = JsonDocument.Parse(json); + JsonElement currentElement = doc.RootElement; + + foreach (var segment in pathSegments) + { + if (!currentElement.TryGetProperty(segment, out JsonElement propertyElement)) + { + return []; + } + + currentElement = propertyElement; + } + + if (currentElement.ValueKind == JsonValueKind.Array) + { + values.AddRange(currentElement.EnumerateArray() + .Where(item => item.ValueKind == JsonValueKind.String) + .Select(item => item.GetString())); + } + else if (currentElement.ValueKind == JsonValueKind.String) + { + values.Add(currentElement.GetString()); + } + } + catch (JsonException) + { + // Handle invalid JSON format + } + + return values.ToArray(); } private void addNewClaims(ClaimsIdentity identity, List claims) diff --git a/src/Caster.Api/Features/Applies/Requests/Execute.cs b/src/Caster.Api/Features/Applies/Requests/Execute.cs index 3267388..9fa1204 100644 --- a/src/Caster.Api/Features/Applies/Requests/Execute.cs +++ b/src/Caster.Api/Features/Applies/Requests/Execute.cs @@ -13,13 +13,12 @@ using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Events; using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Domain.Services; using System.Linq; using Caster.Api.Infrastructure.Identity; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Applies { @@ -35,12 +34,12 @@ public class Command : IRequest public Guid RunId { get; set; } } - public class Handler : IRequestHandler + public class Handler : BaseHandler { private readonly CasterContext _db; private readonly IMapper _mapper; private readonly IMediator _mediator; - private readonly IAuthorizationService _authorizationService; + private readonly ICasterAuthorizationService _authorizationService; private readonly ClaimsPrincipal _user; private readonly ILockService _lockService; @@ -48,7 +47,7 @@ public Handler( CasterContext db, IMapper mapper, IMediator mediator, - IAuthorizationService authorizationService, + ICasterAuthorizationService authorizationService, IIdentityResolver identityResolver, ILockService lockService) { @@ -60,11 +59,11 @@ public Handler( _lockService = lockService; } - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await _authorizationService.Authorize(request.RunId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) + { var workspaceId = await _db.Runs.Where(r => r.Id == request.RunId).Select(r => r.WorkspaceId).FirstOrDefaultAsync(); Domain.Models.Apply apply = null; diff --git a/src/Caster.Api/Features/Applies/Requests/Get.cs b/src/Caster.Api/Features/Applies/Requests/Get.cs index dd80e97..b63638f 100644 --- a/src/Caster.Api/Features/Applies/Requests/Get.cs +++ b/src/Caster.Api/Features/Applies/Requests/Get.cs @@ -16,12 +16,14 @@ using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Infrastructure.Identity; +using Caster.Api.Domain.Models; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Applies { public class Get { - [DataContract(Name="GetApplyQuery")] + [DataContract(Name = "GetApplyQuery")] public class Query : IRequest { /// @@ -31,30 +33,27 @@ public class Query : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler : BaseHandler { private readonly CasterContext _db; private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + private readonly ICasterAuthorizationService _authorizationService; public Handler( CasterContext db, IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + ICasterAuthorizationService authorizationService) { _db = db; _mapper = mapper; _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); } - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await _authorizationService.Authorize(request.Id, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { var apply = await _db.Applies .ProjectTo(_mapper.ConfigurationProvider) .SingleOrDefaultAsync(x => x.Id == request.Id, cancellationToken); diff --git a/src/Caster.Api/Features/Applies/Requests/GetByRun.cs b/src/Caster.Api/Features/Applies/Requests/GetByRun.cs index 5c3b697..673eb08 100644 --- a/src/Caster.Api/Features/Applies/Requests/GetByRun.cs +++ b/src/Caster.Api/Features/Applies/Requests/GetByRun.cs @@ -17,12 +17,13 @@ using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Models; using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Applies { public class GetByRun { - [DataContract(Name="GetApplyByRunQuery")] + [DataContract(Name = "GetApplyByRunQuery")] public class Query : IRequest { /// @@ -32,30 +33,27 @@ public class Query : IRequest public Guid RunId { get; set; } } - public class Handler : IRequestHandler + public class Handler : BaseHandler { private readonly CasterContext _db; private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + private readonly ICasterAuthorizationService _authorizationService; public Handler( CasterContext db, IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + ICasterAuthorizationService authorizationService) { _db = db; _mapper = mapper; _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); } - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await _authorizationService.Authorize(request.RunId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { var run = await _db.Runs.FirstOrDefaultAsync(x => x.Id == request.RunId); if (run == null) diff --git a/src/Caster.Api/Features/DesignModules/MappingProfile.cs b/src/Caster.Api/Features/DesignModules/MappingProfile.cs index 3296829..540a91d 100644 --- a/src/Caster.Api/Features/DesignModules/MappingProfile.cs +++ b/src/Caster.Api/Features/DesignModules/MappingProfile.cs @@ -14,6 +14,7 @@ public MappingProfile() ShouldMapProperty = p => p.GetMethod.IsPublic || p.GetMethod.IsPrivate; CreateMap() + .ForMember(dest => dest.Values, opt => opt.Ignore()) .ForMember("ValuesJson", opt => opt.ExplicitExpansion()); CreateMap(); diff --git a/src/Caster.Api/Features/DesignModules/Requests/AddOrUpdateValues.cs b/src/Caster.Api/Features/DesignModules/Requests/AddOrUpdateValues.cs index f8f3e56..7f00499 100644 --- a/src/Caster.Api/Features/DesignModules/Requests/AddOrUpdateValues.cs +++ b/src/Caster.Api/Features/DesignModules/Requests/AddOrUpdateValues.cs @@ -14,6 +14,9 @@ using FluentValidation; using System.Linq; using Microsoft.EntityFrameworkCore; +using Caster.Api.Domain.Models; +using AutoMapper; +using Caster.Api.Data; namespace Caster.Api.Features.DesignModules; @@ -28,16 +31,19 @@ public record Command : IRequest public ModuleValue[] Values { get; init; } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) + : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } - - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize( + request.DesignModuleId, + [SystemPermission.EditProjects], + [ProjectPermission.EditProject], + cancellationToken); + + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var designModule = await _db.DesignModules + var designModule = await dbContext.DesignModules .Where(x => x.Id == request.DesignModuleId) .SingleOrDefaultAsync(cancellationToken); @@ -45,9 +51,9 @@ public async Task Handle(Command request, CancellationToken cancel throw new EntityNotFoundException(); designModule.AddOrUpdateValues(ModuleValue.ToDomain(request.Values)); - await _db.SaveChangesAsync(cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); - return _mapper.Map(designModule); + return mapper.Map(designModule); } } } diff --git a/src/Caster.Api/Features/DesignModules/Requests/Create.cs b/src/Caster.Api/Features/DesignModules/Requests/Create.cs index 7792852..b05553f 100644 --- a/src/Caster.Api/Features/DesignModules/Requests/Create.cs +++ b/src/Caster.Api/Features/DesignModules/Requests/Create.cs @@ -13,6 +13,9 @@ using FluentValidation; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Domain.Models; +using AutoMapper; +using Caster.Api.Data; namespace Caster.Api.Features.DesignModules; @@ -59,21 +62,19 @@ public Validator(IValidationService validationService) } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.DesignId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); + var designModule = mapper.Map(request); - var designModule = _mapper.Map(request); + dbContext.DesignModules.Add(designModule); + await dbContext.SaveChangesAsync(cancellationToken); - _db.DesignModules.Add(designModule); - await _db.SaveChangesAsync(cancellationToken); - - return _mapper.Map(designModule); + return mapper.Map(designModule); } } } diff --git a/src/Caster.Api/Features/DesignModules/Requests/Delete.cs b/src/Caster.Api/Features/DesignModules/Requests/Delete.cs index 66048d0..aac6e7e 100644 --- a/src/Caster.Api/Features/DesignModules/Requests/Delete.cs +++ b/src/Caster.Api/Features/DesignModules/Requests/Delete.cs @@ -11,6 +11,8 @@ using Caster.Api.Infrastructure.Authorization; using Caster.Api.Features.Shared; using System.Text.Json.Serialization; +using Caster.Api.Domain.Models; +using Caster.Api.Data; namespace Caster.Api.Features.DesignModules; @@ -23,22 +25,20 @@ public record Command : IRequest public Guid Id { get; set; } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public async override Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var designModule = await _db.DesignModules.FindAsync(request.Id); + var designModule = await dbContext.DesignModules.FindAsync(request.Id); if (designModule == null) throw new EntityNotFoundException(); - _db.DesignModules.Remove(designModule); - await _db.SaveChangesAsync(cancellationToken); + dbContext.DesignModules.Remove(designModule); + await dbContext.SaveChangesAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/DesignModules/Requests/Edit.cs b/src/Caster.Api/Features/DesignModules/Requests/Edit.cs index 9bcc961..27e7d09 100644 --- a/src/Caster.Api/Features/DesignModules/Requests/Edit.cs +++ b/src/Caster.Api/Features/DesignModules/Requests/Edit.cs @@ -16,6 +16,9 @@ using System.Linq; using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; +using Caster.Api.Domain.Models; +using Caster.Api.Data; +using AutoMapper; namespace Caster.Api.Features.DesignModules; @@ -65,26 +68,24 @@ public Validator(IValidationService validationService) } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.DesignModuleId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var designModule = await _db.DesignModules + var designModule = await dbContext.DesignModules .Where(x => x.Id == request.DesignModuleId) .SingleOrDefaultAsync(cancellationToken); if (designModule == null) throw new EntityNotFoundException(); - _mapper.Map(request, designModule); - await _db.SaveChangesAsync(cancellationToken); + mapper.Map(request, designModule); + await dbContext.SaveChangesAsync(cancellationToken); - return _mapper.Map(designModule); + return mapper.Map(designModule); } } } diff --git a/src/Caster.Api/Features/DesignModules/Requests/Get.cs b/src/Caster.Api/Features/DesignModules/Requests/Get.cs index 6770787..f860bae 100644 --- a/src/Caster.Api/Features/DesignModules/Requests/Get.cs +++ b/src/Caster.Api/Features/DesignModules/Requests/Get.cs @@ -15,6 +15,9 @@ using Caster.Api.Features.Shared; using FluentValidation; using AutoMapper.QueryableExtensions; +using Caster.Api.Domain.Models; +using AutoMapper; +using Caster.Api.Data; namespace Caster.Api.Features.DesignModules; @@ -27,18 +30,16 @@ public record Query : IRequest public Guid Id { get; set; } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public async Task Handle(Query request, CancellationToken cancellationToken) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var designModule = await _db.DesignModules + var designModule = await dbContext.DesignModules .Where(x => x.Id == request.Id) - .ProjectTo(_mapper.ConfigurationProvider, null, membersToExpand: new[] { "ValuesJson" }) + .ProjectTo(mapper.ConfigurationProvider, null, membersToExpand: new[] { "ValuesJson" }) .SingleOrDefaultAsync(cancellationToken); if (designModule == null) diff --git a/src/Caster.Api/Features/DesignModules/Requests/GetByDesign.cs b/src/Caster.Api/Features/DesignModules/Requests/GetByDesign.cs index 537bf06..4b95aa4 100644 --- a/src/Caster.Api/Features/DesignModules/Requests/GetByDesign.cs +++ b/src/Caster.Api/Features/DesignModules/Requests/GetByDesign.cs @@ -17,6 +17,9 @@ using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; using AutoMapper.QueryableExtensions; +using Caster.Api.Domain.Models; +using AutoMapper; +using Caster.Api.Data; namespace Caster.Api.Features.DesignModules; @@ -37,18 +40,16 @@ public Validator(IValidationService validationService) } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.DesignId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public async Task Handle(Query request, CancellationToken cancellationToken) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var designModules = await _db.DesignModules + var designModules = await dbContext.DesignModules .Where(x => x.DesignId == request.DesignId) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .ToArrayAsync(); return designModules; diff --git a/src/Caster.Api/Features/DesignModules/Requests/SetEnabled.cs b/src/Caster.Api/Features/DesignModules/Requests/SetEnabled.cs index 66a25d2..bef3bc3 100644 --- a/src/Caster.Api/Features/DesignModules/Requests/SetEnabled.cs +++ b/src/Caster.Api/Features/DesignModules/Requests/SetEnabled.cs @@ -16,6 +16,9 @@ using System.Linq; using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; +using Caster.Api.Domain.Models; +using AutoMapper; +using Caster.Api.Data; namespace Caster.Api.Features.DesignModules; @@ -30,16 +33,18 @@ public record Command : IRequest public bool Enabled { get; init; } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } - - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize( + request.DesignModuleId, + [SystemPermission.EditProjects], + [ProjectPermission.EditProject], + cancellationToken); + + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var designModule = await _db.DesignModules + var designModule = await dbContext.DesignModules .Where(x => x.Id == request.DesignModuleId) .SingleOrDefaultAsync(cancellationToken); @@ -47,9 +52,9 @@ public async Task Handle(Command request, CancellationToken cancel throw new EntityNotFoundException(); designModule.Enabled = request.Enabled; - await _db.SaveChangesAsync(cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); - return _mapper.Map(designModule); + return mapper.Map(designModule); } } } diff --git a/src/Caster.Api/Features/Designs/Requests/Create.cs b/src/Caster.Api/Features/Designs/Requests/Create.cs index 35c3f12..e6aa3b5 100644 --- a/src/Caster.Api/Features/Designs/Requests/Create.cs +++ b/src/Caster.Api/Features/Designs/Requests/Create.cs @@ -6,13 +6,14 @@ using System.Threading.Tasks; using MediatR; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Features.Shared; using FluentValidation; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using AutoMapper; +using Caster.Api.Data; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Designs; @@ -42,21 +43,19 @@ public Validator(IValidationService validationService) } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.DirectoryId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); + var design = mapper.Map(request); - var design = _mapper.Map(request); + dbContext.Designs.Add(design); + await dbContext.SaveChangesAsync(cancellationToken); - _db.Designs.Add(design); - await _db.SaveChangesAsync(cancellationToken); - - return _mapper.Map(design); + return mapper.Map(design); } } } diff --git a/src/Caster.Api/Features/Designs/Requests/Delete.cs b/src/Caster.Api/Features/Designs/Requests/Delete.cs index 5a3b51b..4a31d2b 100644 --- a/src/Caster.Api/Features/Designs/Requests/Delete.cs +++ b/src/Caster.Api/Features/Designs/Requests/Delete.cs @@ -14,6 +14,8 @@ using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; using System.Text.Json.Serialization; +using Caster.Api.Data; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Designs; @@ -26,22 +28,20 @@ public record Command : IRequest public Guid Id { get; set; } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var design = await _db.Designs.FindAsync(request.Id); + var design = await dbContext.Designs.FindAsync(request.Id); if (design == null) throw new EntityNotFoundException(); - _db.Designs.Remove(design); - await _db.SaveChangesAsync(cancellationToken); + dbContext.Designs.Remove(design); + await dbContext.SaveChangesAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Designs/Requests/Edit.cs b/src/Caster.Api/Features/Designs/Requests/Edit.cs index 0fe9f47..da151fc 100644 --- a/src/Caster.Api/Features/Designs/Requests/Edit.cs +++ b/src/Caster.Api/Features/Designs/Requests/Edit.cs @@ -16,6 +16,9 @@ using System.Linq; using Microsoft.EntityFrameworkCore; using System.Text.Json.Serialization; +using AutoMapper; +using Caster.Api.Data; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Designs; @@ -34,27 +37,25 @@ public record Command : IRequest public string Name { get; init; } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var design = await _db.Designs + var design = await dbContext.Designs .Where(x => x.Id == request.Id) .SingleOrDefaultAsync(cancellationToken); if (design == null) throw new EntityNotFoundException(); - _mapper.Map(request, design); + mapper.Map(request, design); - await _db.SaveChangesAsync(cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); - return _mapper.Map(design); + return mapper.Map(design); } } } diff --git a/src/Caster.Api/Features/Designs/Requests/Get.cs b/src/Caster.Api/Features/Designs/Requests/Get.cs index 3ea8a62..0a2eb40 100644 --- a/src/Caster.Api/Features/Designs/Requests/Get.cs +++ b/src/Caster.Api/Features/Designs/Requests/Get.cs @@ -8,13 +8,15 @@ using Microsoft.EntityFrameworkCore; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using System.Text.Json.Serialization; using System.Linq; using Caster.Api.Features.Shared; using FluentValidation; using AutoMapper.QueryableExtensions; +using AutoMapper; +using Caster.Api.Data; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Designs; @@ -27,18 +29,16 @@ public record Query : IRequest public Guid Id { get; set; } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public async Task Handle(Query request, CancellationToken cancellationToken) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var design = await _db.Designs + var design = await dbContext.Designs .Where(x => x.Id == request.Id) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .SingleOrDefaultAsync(cancellationToken); if (design == null) diff --git a/src/Caster.Api/Features/Designs/Requests/GetByDirectory.cs b/src/Caster.Api/Features/Designs/Requests/GetByDirectory.cs index 63e8c8f..8ea0763 100644 --- a/src/Caster.Api/Features/Designs/Requests/GetByDirectory.cs +++ b/src/Caster.Api/Features/Designs/Requests/GetByDirectory.cs @@ -7,8 +7,6 @@ using MediatR; using Microsoft.EntityFrameworkCore; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using System.Text.Json.Serialization; using System.Linq; @@ -16,6 +14,9 @@ using FluentValidation; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using AutoMapper; +using Caster.Api.Data; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Designs; @@ -36,20 +37,18 @@ public Validator(IValidationService validationService) } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.DirectoryId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public async Task Handle(Query request, CancellationToken cancellationToken) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var designs = await _db.Designs + var designs = await dbContext.Designs .Where(x => x.DirectoryId == request.DirectoryId) .ToArrayAsync(); - return _mapper.Map(designs); + return mapper.Map(designs); } } } \ No newline at end of file diff --git a/src/Caster.Api/Features/Designs/Requests/SetEnabled.cs b/src/Caster.Api/Features/Designs/Requests/SetEnabled.cs index ea0a037..3f46db7 100644 --- a/src/Caster.Api/Features/Designs/Requests/SetEnabled.cs +++ b/src/Caster.Api/Features/Designs/Requests/SetEnabled.cs @@ -14,6 +14,9 @@ using System.Linq; using Microsoft.EntityFrameworkCore; using System.Text.Json.Serialization; +using AutoMapper; +using Caster.Api.Data; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Designs; @@ -29,16 +32,14 @@ public record Command : IRequest public bool Enabled { get; set; } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var design = await _db.Designs + var design = await dbContext.Designs .Where(x => x.Id == request.Id) .SingleOrDefaultAsync(cancellationToken); @@ -46,9 +47,9 @@ public async Task Handle(Command request, CancellationToken cancellation throw new EntityNotFoundException(); design.Enabled = request.Enabled; - await _db.SaveChangesAsync(cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); - return _mapper.Map(design); + return mapper.Map(design); } } } diff --git a/src/Caster.Api/Features/Directories/Requests/BaseEdit.cs b/src/Caster.Api/Features/Directories/Requests/BaseEdit.cs index 4ee6e6c..d22880e 100644 --- a/src/Caster.Api/Features/Directories/Requests/BaseEdit.cs +++ b/src/Caster.Api/Features/Directories/Requests/BaseEdit.cs @@ -8,20 +8,21 @@ using Caster.Api.Infrastructure.Exceptions; using Microsoft.EntityFrameworkCore; using Caster.Api.Data.Extensions; +using Caster.Api.Features.Shared; +using MediatR; namespace Caster.Api.Features.Directories { public abstract class BaseEdit { - public abstract class Handler + public abstract class Handler : BaseHandler + where TRequest : IRequest { - protected readonly CasterContext _db; - protected readonly IMapper _mapper; + protected readonly CasterContext dbContext; - public Handler(CasterContext db, IMapper mapper) + public Handler(CasterContext db) { - _db = db; - _mapper = mapper; + dbContext = db; } protected async Task UpdatePaths(Domain.Models.Directory directory, Guid? parentId) @@ -31,7 +32,7 @@ protected async Task UpdatePaths(Domain.Models.Directory directory, Guid? parent if (parentId.HasValue) { - var parentDirectory = await _db.Directories.FindAsync(parentId); + var parentDirectory = await dbContext.Directories.FindAsync(parentId); if (parentDirectory == null) throw new EntityNotFoundException("Parent Directory Not Found"); @@ -39,11 +40,11 @@ protected async Task UpdatePaths(Domain.Models.Directory directory, Guid? parent parentPath = parentDirectory.Path; } - var descendants = await _db.Directories.GetChildren(directory, false).ToListAsync(); + var descendants = await dbContext.Directories.GetChildren(directory, false).ToListAsync(); directory.SetPath(parentPath); - foreach(var desc in descendants) + foreach (var desc in descendants) { desc.Path = desc.Path.Replace(oldPath, directory.Path); } diff --git a/src/Caster.Api/Features/Directories/Requests/Create.cs b/src/Caster.Api/Features/Directories/Requests/Create.cs index 285bd0d..e3734a4 100644 --- a/src/Caster.Api/Features/Directories/Requests/Create.cs +++ b/src/Caster.Api/Features/Directories/Requests/Create.cs @@ -23,6 +23,7 @@ using Caster.Api.Features.Shared.Validators; using Microsoft.Extensions.Options; using Caster.Api.Infrastructure.Options; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Directories { @@ -58,7 +59,7 @@ public class Command : IRequest, IDirectoryUpdateRequest public string TerraformVersion { get; set; } /// - /// Limit the number of concurrent operations as Terraform walks the graph. + /// Limit the number of concurrent operations as Terraform walks the graph. /// If not set, will traverse parents until a value is found. /// If still not set, the Terraform default will be used. /// @@ -66,7 +67,7 @@ public class Command : IRequest, IDirectoryUpdateRequest public int? Parallelism { get; set; } /// - /// If set, the number of consecutive failed destroys in an Azure Workspace before + /// If set, the number of consecutive failed destroys in an Azure Workspace before /// Caster will attempt to mitigate by removing azurerm_resource_group children from the state. /// If not set, will traverse parents until a value is found. /// @@ -94,36 +95,19 @@ public CommandValidator(IValidationService validationService, TerraformOptions o } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.ProjectId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var directory = _mapper.Map(request); + var directory = mapper.Map(request); await SetPath(directory); - _db.Directories.Add(directory); - await _db.SaveChangesAsync(cancellationToken); - return _mapper.Map(directory); + dbContext.Directories.Add(directory); + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(directory); } private async Task SetPath(Domain.Models.Directory directory) @@ -136,7 +120,7 @@ private async Task SetPath(Domain.Models.Directory directory) } else { - var parentDirectory = await _db.Directories.FindAsync(directory.ParentId); + var parentDirectory = await dbContext.Directories.FindAsync(directory.ParentId); if (parentDirectory == null) throw new EntityNotFoundException("Parent Directory not found"); diff --git a/src/Caster.Api/Features/Directories/Requests/Delete.cs b/src/Caster.Api/Features/Directories/Requests/Delete.cs index 3de48d0..1d26bb5 100644 --- a/src/Caster.Api/Features/Directories/Requests/Delete.cs +++ b/src/Caster.Api/Features/Directories/Requests/Delete.cs @@ -11,12 +11,11 @@ using Caster.Api.Infrastructure.Exceptions; using Microsoft.EntityFrameworkCore; using System.Collections.Generic; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using Caster.Api.Features.Directories.Interfaces; using Caster.Api.Data.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Directories { @@ -28,29 +27,14 @@ public class Command : IRequest, IDirectoryDeleteRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public Handler( - CasterContext db, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - IMediator mediator) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var directory = await _db.Directories.FindAsync(request.Id); + var directory = await dbContext.Directories.FindAsync(request.Id); if (directory == null) throw new EntityNotFoundException(); @@ -69,18 +53,18 @@ public async Task Handle(Command request, CancellationToken cancellationToken) throw new ConflictException(errorMessage); } - _db.Directories.Remove(directory); - await _db.SaveChangesAsync(cancellationToken); + dbContext.Directories.Remove(directory); + await dbContext.SaveChangesAsync(cancellationToken); } - private async Task CheckForResources(Domain.Models.Directory directory) + private async Task CheckForResources(Domain.Models.Directory directory) { - var directories = await _db.Directories + var directories = await dbContext.Directories .GetChildren(directory, true) .Include(d => d.Workspaces) .ToArrayAsync(); - List workspaces = new List(); + List workspaces = new List(); foreach (var workspace in directories.SelectMany(d => d.Workspaces)) { diff --git a/src/Caster.Api/Features/Directories/Requests/Edit.cs b/src/Caster.Api/Features/Directories/Requests/Edit.cs index 8b78e89..be9b585 100644 --- a/src/Caster.Api/Features/Directories/Requests/Edit.cs +++ b/src/Caster.Api/Features/Directories/Requests/Edit.cs @@ -20,6 +20,7 @@ using Caster.Api.Features.Shared.Services; using Caster.Api.Features.Shared.Validators; using Caster.Api.Infrastructure.Options; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Directories { @@ -87,22 +88,14 @@ public CommandValidator(IValidationService validationService, TerraformOptions o } } - public class Handler : BaseEdit.Handler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext db) : BaseEdit.Handler(db) { - protected readonly IAuthorizationService _authorizationService; - protected readonly ClaimsPrincipal _user; - public Handler(CasterContext db, IMapper mapper, IAuthorizationService authorizationService, IIdentityResolver identityResolver) : base(db, mapper) - { - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var directory = await _db.Directories.FindAsync(request.Id); + var directory = await dbContext.Directories.FindAsync(request.Id); if (directory == null) throw new EntityNotFoundException(); @@ -112,10 +105,10 @@ public async Task Handle(Command request, CancellationToken cancellat await UpdatePaths(directory, request.ParentId); } - _mapper.Map(request, directory); + mapper.Map(request, directory); - await _db.SaveChangesAsync(); - return _mapper.Map(directory); + await dbContext.SaveChangesAsync(); + return mapper.Map(directory); } } } diff --git a/src/Caster.Api/Features/Directories/Requests/Export.cs b/src/Caster.Api/Features/Directories/Requests/Export.cs index 3307b8c..5b760ff 100644 --- a/src/Caster.Api/Features/Directories/Requests/Export.cs +++ b/src/Caster.Api/Features/Directories/Requests/Export.cs @@ -17,12 +17,13 @@ using System.Text.Json.Serialization; using Caster.Api.Domain.Services; using Caster.Api.Domain.Models; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Directories { public class Export { - [DataContract(Name="ExportDirectoryQuery")] + [DataContract(Name = "ExportDirectoryQuery")] public class Query : IRequest { [JsonIgnore] @@ -36,41 +37,21 @@ public class Query : IRequest public bool IncludeIds { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext, IArchiveService archiveService) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly IArchiveService _archiveService; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - IArchiveService archiveService) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _archiveService = archiveService; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var directory = await _db.Directories + var directory = await dbContext.Directories .SingleOrDefaultAsync(e => e.Id == request.Id, cancellationToken); if (directory == null) throw new EntityNotFoundException(); - var directories = await _db.GetDirectoryWithChildren(directory.Id, cancellationToken); - return await _archiveService.ArchiveDirectory(directory, request.ArchiveType, request.IncludeIds); + var directories = await dbContext.GetDirectoryWithChildren(directory.Id, cancellationToken); + return await archiveService.ArchiveDirectory(directory, request.ArchiveType, request.IncludeIds); } } } diff --git a/src/Caster.Api/Features/Directories/Requests/Get.cs b/src/Caster.Api/Features/Directories/Requests/Get.cs index a4683cb..8fe60e8 100644 --- a/src/Caster.Api/Features/Directories/Requests/Get.cs +++ b/src/Caster.Api/Features/Directories/Requests/Get.cs @@ -10,18 +10,16 @@ using Microsoft.EntityFrameworkCore; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using System.Text.Json.Serialization; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Directories { public class Get { - [DataContract(Name="GetDirectoryQuery")] + [DataContract(Name = "GetDirectoryQuery")] public class Query : IRequest { [JsonIgnore] @@ -40,32 +38,15 @@ public class Query : IRequest public bool IncludeFileContent { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var directory = await _db.Directories - .Expand(_mapper.ConfigurationProvider, request.IncludeRelated, request.IncludeFileContent) + var directory = await dbContext.Directories + .Expand(mapper.ConfigurationProvider, request.IncludeRelated, request.IncludeFileContent) .SingleOrDefaultAsync(e => e.Id == request.Id, cancellationToken); if (directory == null) diff --git a/src/Caster.Api/Features/Directories/Requests/GetAll.cs b/src/Caster.Api/Features/Directories/Requests/GetAll.cs index b94630a..06342d7 100644 --- a/src/Caster.Api/Features/Directories/Requests/GetAll.cs +++ b/src/Caster.Api/Features/Directories/Requests/GetAll.cs @@ -10,16 +10,17 @@ using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; using System.Security.Claims; -using System.Security.Principal; using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Directories { public class GetAll { - [DataContract(Name="GetDirectoriesQuery")] + [DataContract(Name = "GetDirectoriesQuery")] public class Query : IRequest { /// @@ -35,32 +36,15 @@ public class Query : IRequest public bool IncludeFileContent { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewProjects], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - return await _db.Directories - .Expand(_mapper.ConfigurationProvider, request.IncludeRelated, request.IncludeFileContent) + return await dbContext.Directories + .Expand(mapper.ConfigurationProvider, request.IncludeRelated, request.IncludeFileContent) .ToArrayAsync(); } } diff --git a/src/Caster.Api/Features/Directories/Requests/GetByExercise.cs b/src/Caster.Api/Features/Directories/Requests/GetByExercise.cs deleted file mode 100644 index 178ffec..0000000 --- a/src/Caster.Api/Features/Directories/Requests/GetByExercise.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System; -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using AutoMapper; -using Caster.Api.Data; -using System.Linq; -using Microsoft.EntityFrameworkCore; -using System.Runtime.Serialization; -using Caster.Api.Domain.Models; -using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; -using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; -using System.Text.Json.Serialization; - -namespace Caster.Api.Features.Directories -{ - public class GetByProject - { - [DataContract(Name="GetDirectoriesByProjectQuery")] - public class Query : IRequest - { - [JsonIgnore] - public Guid ProjectId { get; set; } - - /// - /// Whether or not to return only top-level Directories - /// - [DataMember] - public bool IncludeDescendants { get; set; } - - /// - /// Whether or not to return related objects (Files, Workspaces) - /// - [DataMember] - public bool IncludeRelated { get; set; } - - /// - /// Whether or not to include contents of returned Files. Ignored if IncludeRelated is false - /// - [DataMember] - public bool IncludeFileContent { get; set; } - } - - public class Handler : IRequestHandler - { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - await ValidateProject(request.ProjectId); - - var query = _db.Directories.Where(d => d.ProjectId == request.ProjectId); - - if (!request.IncludeDescendants) - query = query.Where(d => d.ParentId == null); - - var modifiedQuery = query.Expand(_mapper.ConfigurationProvider, request.IncludeRelated, request.IncludeFileContent); - var directories = await modifiedQuery.ToArrayAsync(); - - return directories; - } - - private async Task ValidateProject(Guid projectId) - { - var project = await _db.Projects.FindAsync(projectId); - - if (project == null) - throw new EntityNotFoundException(); - } - } - } -} diff --git a/src/Caster.Api/Features/Directories/Requests/GetByProject.cs b/src/Caster.Api/Features/Directories/Requests/GetByProject.cs new file mode 100644 index 0000000..fa5c7d3 --- /dev/null +++ b/src/Caster.Api/Features/Directories/Requests/GetByProject.cs @@ -0,0 +1,77 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using AutoMapper; +using Caster.Api.Data; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using System.Runtime.Serialization; +using Caster.Api.Domain.Models; +using Caster.Api.Infrastructure.Authorization; +using System.Text.Json.Serialization; +using Caster.Api.Features.Shared; +using FluentValidation; +using Caster.Api.Features.Shared.Services; +using Caster.Api.Infrastructure.Extensions; + +namespace Caster.Api.Features.Directories +{ + public class GetByProject + { + [DataContract(Name = "GetDirectoriesByProjectQuery")] + public class Query : IRequest + { + [JsonIgnore] + public Guid ProjectId { get; set; } + + /// + /// Whether or not to return only top-level Directories + /// + [DataMember] + public bool IncludeDescendants { get; set; } + + /// + /// Whether or not to return related objects (Files, Workspaces) + /// + [DataMember] + public bool IncludeRelated { get; set; } + + /// + /// Whether or not to include contents of returned Files. Ignored if IncludeRelated is false + /// + [DataMember] + public bool IncludeFileContent { get; set; } + } + + public class Validator : AbstractValidator + { + public Validator(IValidationService validationService) + { + RuleFor(x => x.ProjectId).ProjectExists(validationService); + } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.ProjectId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); + + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { + var query = dbContext.Directories.Where(d => d.ProjectId == request.ProjectId); + + if (!request.IncludeDescendants) + query = query.Where(d => d.ParentId == null); + + var modifiedQuery = query.Expand(mapper.ConfigurationProvider, request.IncludeRelated, request.IncludeFileContent); + var directories = await modifiedQuery.ToArrayAsync(); + + return directories; + } + } + } +} diff --git a/src/Caster.Api/Features/Directories/Requests/GetChildren.cs b/src/Caster.Api/Features/Directories/Requests/GetChildren.cs index 19b6980..7a1d6da 100644 --- a/src/Caster.Api/Features/Directories/Requests/GetChildren.cs +++ b/src/Caster.Api/Features/Directories/Requests/GetChildren.cs @@ -17,12 +17,14 @@ using Caster.Api.Infrastructure.Identity; using System.Text.Json.Serialization; using Caster.Api.Data.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Directories { public class GetChildren { - [DataContract(Name="GetDirectoryChildrenQuery")] + [DataContract(Name = "GetDirectoryChildrenQuery")] public class Query : IRequest { [JsonIgnore] @@ -47,31 +49,14 @@ public class Query : IRequest public bool IncludeFileContent { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.DirectoryId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var directory = await _db.Directories.FindAsync(request.DirectoryId); + var directory = await dbContext.Directories.FindAsync(request.DirectoryId); if (directory == null) throw new EntityNotFoundException(); @@ -80,14 +65,14 @@ public async Task Handle(Query request, CancellationToken cancellat if (request.IncludeDescendants) { - query = _db.Directories.GetChildren(directory, false); + query = dbContext.Directories.GetChildren(directory, false); } else { - query = _db.Directories.Where(d => d.ParentId == directory.Id); + query = dbContext.Directories.Where(d => d.ParentId == directory.Id); } - var modifiedQuery = query.Expand(_mapper.ConfigurationProvider, request.IncludeRelated, request.IncludeFileContent); + var modifiedQuery = query.Expand(mapper.ConfigurationProvider, request.IncludeRelated, request.IncludeFileContent); var directories = await modifiedQuery.ToArrayAsync(); return directories; diff --git a/src/Caster.Api/Features/Directories/Requests/Import.cs b/src/Caster.Api/Features/Directories/Requests/Import.cs index 51e95de..b8fa6dc 100644 --- a/src/Caster.Api/Features/Directories/Requests/Import.cs +++ b/src/Caster.Api/Features/Directories/Requests/Import.cs @@ -10,24 +10,21 @@ using Microsoft.EntityFrameworkCore; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using System.Text.Json.Serialization; using Caster.Api.Domain.Services; using Microsoft.AspNetCore.Http; using FluentValidation; -using System.Linq; using Caster.Api.Domain.Models; using Microsoft.EntityFrameworkCore.ChangeTracking; using Caster.Api.Data.Extensions; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Directories { public class Import { - [DataContract(Name="ImportDirectoryCommand")] + [DataContract(Name = "ImportDirectoryCommand")] public class Command : IRequest { [JsonIgnore] @@ -40,8 +37,10 @@ public class Command : IRequest public bool PreserveIds { get; set; } } - public class ImportValidator : AbstractValidator { - public ImportValidator() { + public class ImportValidator : AbstractValidator + { + public ImportValidator() + { RuleFor(x => x.Archive) .NotNull().Must(BeAValidArchiveType) .WithMessage($"File extension must be one of {string.Join(", ", ArchiveTypeHelpers.GetValidExtensions())}"); @@ -72,40 +71,20 @@ public class ImportDirectoryResult public string[] LockedFiles { get; set; } } - public class Handler : IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + IArchiveService archiveService, + IImportService importService, + IMediator mediator) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly IArchiveService _archiveService; - private readonly IImportService _importService; - private readonly IMediator _mediator; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - IArchiveService archiveService, - IImportService importService, - IMediator mediator) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _archiveService = archiveService; - _importService = importService; - _mediator = mediator; - } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var directory = await _db.Directories + var directory = await dbContext.Directories .SingleOrDefaultAsync(e => e.Id == request.Id, cancellationToken); if (directory == null) @@ -117,17 +96,17 @@ public async Task Handle(Command request, CancellationTok { await request.Archive.CopyToAsync(memStream, cancellationToken); memStream.Position = 0; - extractedDirectory = _archiveService.ExtractDirectory(memStream, request.Archive.FileName); + extractedDirectory = archiveService.ExtractDirectory(memStream, request.Archive.FileName); } - var directories = await _db.GetDirectoryWithChildren(directory.Id, cancellationToken); - var importResult = await _importService.ImportDirectory(directory, extractedDirectory, request.PreserveIds); + var directories = await dbContext.GetDirectoryWithChildren(directory.Id, cancellationToken); + var importResult = await importService.ImportDirectory(directory, extractedDirectory, request.PreserveIds); - var entries = _db.GetUpdatedEntries(); - await _db.SaveChangesAsync(cancellationToken); + var entries = dbContext.GetUpdatedEntries(); + await dbContext.SaveChangesAsync(cancellationToken); await this.PublishEvents(entries); - return _mapper.Map(importResult); + return mapper.Map(importResult); } private async Task PublishEvents(EntityEntry[] entries) @@ -138,7 +117,7 @@ private async Task PublishEvents(EntityEntry[] entries) if (evt != null) { - await _mediator.Publish(evt); + await mediator.Publish(evt); } } } diff --git a/src/Caster.Api/Features/Directories/Requests/PartialEdit.cs b/src/Caster.Api/Features/Directories/Requests/PartialEdit.cs index 60d9d57..a8c0a03 100644 --- a/src/Caster.Api/Features/Directories/Requests/PartialEdit.cs +++ b/src/Caster.Api/Features/Directories/Requests/PartialEdit.cs @@ -21,6 +21,7 @@ using System.Text.Json.Serialization; using Caster.Api.Features.Shared.Validators; using Caster.Api.Infrastructure.Options; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Directories { @@ -89,22 +90,14 @@ public CommandValidator(IValidationService validationService, TerraformOptions o } } - public class Handler : BaseEdit.Handler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext db) : BaseEdit.Handler(db) { - protected readonly IAuthorizationService _authorizationService; - protected readonly ClaimsPrincipal _user; - public Handler(CasterContext db, IMapper mapper, IAuthorizationService authorizationService, IIdentityResolver identityResolver) : base(db, mapper) - { - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var directory = await _db.Directories.FindAsync(request.Id); + var directory = await dbContext.Directories.FindAsync(request.Id); if (directory == null) throw new EntityNotFoundException(); @@ -114,10 +107,10 @@ public async Task Handle(Command request, CancellationToken cancellat await UpdatePaths(directory, request.ParentId.Value); } - _mapper.Map(request, directory); + mapper.Map(request, directory); - await _db.SaveChangesAsync(); - return _mapper.Map(directory); + await dbContext.SaveChangesAsync(); + return mapper.Map(directory); } } } diff --git a/src/Caster.Api/Features/Files/Requests/AdminLock.cs b/src/Caster.Api/Features/Files/Requests/AdminLock.cs index dadef2d..b53a413 100644 --- a/src/Caster.Api/Features/Files/Requests/AdminLock.cs +++ b/src/Caster.Api/Features/Files/Requests/AdminLock.cs @@ -6,54 +6,34 @@ using System.Threading.Tasks; using MediatR; using Caster.Api.Data; -using AutoMapper; using System.Runtime.Serialization; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Domain.Services; -using Caster.Api.Infrastructure.Identity; -using Caster.Api.Infrastructure.Extensions; using Caster.Api.Features.Files.Interfaces; -using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Files { public class AdminLock { - [DataContract(Name="AdministrativelyLockFileCommand")] + [DataContract(Name = "AdministrativelyLockFileCommand")] public class Command : FileMetadataUpdateRequest, IRequest, IFileCommand { public Guid Id { get; set; } } - public class Handler : FileCommandHandler, IRequestHandler + public class Handler( + CasterContext dbContext, + ILockService lockService, + IGetFileQuery fileQuery, + ICasterAuthorizationService authorizationService) : FileCommandHandler(dbContext, lockService, fileQuery, authorizationService) { - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService, - IGetFileQuery fileQuery) - : base(db, mapper, authorizationService, identityResolver, lockService, fileQuery) {} + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await AuthorizationService.Authorize(request.Id, [SystemPermission.LockFiles], [ProjectPermission.LockFiles], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + protected override async Task PerformOperation(Domain.Models.File file, CancellationToken cancellationToken) { - return await base.Handle(request, cancellationToken); - } - - protected override async Task PerformOperation(Domain.Models.File file) - { - file.AdministrativelyLock((await _identityResolver.IsAdminAsync())); - } - - protected override async Task Authorize() - { - if (!(await _authorizationService.AuthorizeAsync( - _user, null, new FullRightsRequirement())).Succeeded) - { - throw new ForbiddenException(); - } + file.AdministrativelyLock(await CanLock(file.Id, cancellationToken)); } } } diff --git a/src/Caster.Api/Features/Files/Requests/AdminUnlock.cs b/src/Caster.Api/Features/Files/Requests/AdminUnlock.cs index 94f6db6..9fb10e7 100644 --- a/src/Caster.Api/Features/Files/Requests/AdminUnlock.cs +++ b/src/Caster.Api/Features/Files/Requests/AdminUnlock.cs @@ -6,54 +6,34 @@ using System.Threading.Tasks; using MediatR; using Caster.Api.Data; -using AutoMapper; using System.Runtime.Serialization; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Domain.Services; -using Caster.Api.Infrastructure.Identity; -using Caster.Api.Infrastructure.Extensions; using Caster.Api.Features.Files.Interfaces; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Files { public class AdminUnlock { - [DataContract(Name="AdministrativelyUnlockFileCommand")] + [DataContract(Name = "AdministrativelyUnlockFileCommand")] public class Command : FileMetadataUpdateRequest, IRequest, IFileCommand { public Guid Id { get; set; } } - public class Handler : FileCommandHandler, IRequestHandler + public class Handler( + CasterContext dbContext, + ILockService lockService, + IGetFileQuery fileQuery, + ICasterAuthorizationService authorizationService) : FileCommandHandler(dbContext, lockService, fileQuery, authorizationService) { - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService, - IGetFileQuery fileQuery) - : base(db, mapper, authorizationService, identityResolver, lockService, fileQuery) {} + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await AuthorizationService.Authorize(request.Id, [SystemPermission.LockFiles], [ProjectPermission.LockFiles], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + protected override async Task PerformOperation(Domain.Models.File file, CancellationToken cancellationToken) { - return await base.Handle(request, cancellationToken); - } - - protected override async Task PerformOperation(Domain.Models.File file) - { - file.AdministrativelyUnlock((await _identityResolver.IsAdminAsync())); - } - - protected override async Task Authorize() - { - if (!(await _authorizationService.AuthorizeAsync( - _user, null, new FullRightsRequirement())).Succeeded) - { - throw new ForbiddenException(); - } + file.AdministrativelyUnlock(await CanLock(file.Id, cancellationToken)); } } } diff --git a/src/Caster.Api/Features/Files/Requests/Create.cs b/src/Caster.Api/Features/Files/Requests/Create.cs index 4a73300..a8213cc 100644 --- a/src/Caster.Api/Features/Files/Requests/Create.cs +++ b/src/Caster.Api/Features/Files/Requests/Create.cs @@ -16,12 +16,15 @@ using Caster.Api.Infrastructure.Extensions; using Caster.Api.Infrastructure.Identity; using Caster.Api.Features.Files.Interfaces; +using FluentValidation; +using Caster.Api.Features.Shared.Services; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Files { public class Create { - [DataContract(Name="CreateFileCommand")] + [DataContract(Name = "CreateFileCommand")] public class Command : FileUpdateRequest, IRequest { /// @@ -49,60 +52,37 @@ public class Command : FileUpdateRequest, IRequest public override string Content { get; set; } } - public class Handler : IRequestHandler + public class CommandValidator : AbstractValidator { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly IGetFileQuery _fileQuery; - private readonly IIdentityResolver _identityResolver; + public CommandValidator(IValidationService validationService) + { + RuleFor(x => x.DirectoryId).DirectoryExists(validationService); + RuleFor(x => x.WorkspaceId.Value).WorkspaceExists(validationService).When(x => x.WorkspaceId.HasValue); + } + } - public Handler( + public class Handler( CasterContext db, IMapper mapper, - IAuthorizationService authorizationService, + ICasterAuthorizationService authorizationService, IIdentityResolver identityResolver, - IGetFileQuery fileQuery) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _fileQuery = fileQuery; - _identityResolver = identityResolver; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - await ValidateEntities(request.DirectoryId, request.WorkspaceId); - - var file = _mapper.Map(request); - file.Save(_user.GetId(), isAdmin: (await _identityResolver.IsAdminAsync()), bypassLock: true); - - await _db.Files.AddAsync(file, cancellationToken); - await _db.SaveChangesAsync(cancellationToken); - - return await _fileQuery.ExecuteAsync(file.Id); - } + IGetFileQuery fileQuery) : BaseHandler + { + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.DirectoryId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - private async Task ValidateEntities(Guid directoryId, Guid? workspaceId) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - var directory = await _db.Directories.FindAsync(directoryId); - - if (directory == null) - throw new EntityNotFoundException(); + var file = mapper.Map(request); + file.Save( + identityResolver.GetId(), + canLock: await authorizationService.Authorize(request.DirectoryId, [SystemPermission.LockFiles], [ProjectPermission.LockFiles], cancellationToken), + bypassLock: true); - if (workspaceId.HasValue) - { - var workspace = await _db.Workspaces.FindAsync(workspaceId.Value); + db.Files.Add(file); + await db.SaveChangesAsync(cancellationToken); - if (workspace == null) - throw new EntityNotFoundException(); - } + return await fileQuery.ExecuteAsync(file.Id); } } } diff --git a/src/Caster.Api/Features/Files/Requests/Delete.cs b/src/Caster.Api/Features/Files/Requests/Delete.cs index 7d89f5d..5d6e693 100644 --- a/src/Caster.Api/Features/Files/Requests/Delete.cs +++ b/src/Caster.Api/Features/Files/Requests/Delete.cs @@ -2,58 +2,47 @@ // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; using MediatR; -using AutoMapper; using Caster.Api.Data; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using Caster.Api.Features.Files.Interfaces; using Caster.Api.Domain.Services; -using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Domain.Models; +using Caster.Api.Infrastructure.Identity; namespace Caster.Api.Features.Files { public class Delete { - [DataContract(Name="DeleteFileCommand")] + [DataContract(Name = "DeleteFileCommand")] public class Command : IRequest, IFileDeleteRequest, IFileCommand { public Guid Id { get; set; } } - public class Handler : FileCommandHandler, IRequestHandler + public class Handler( + CasterContext dbContext, + ILockService lockService, + IGetFileQuery fileQuery, + ICasterAuthorizationService authorizationService, + IIdentityResolver identityResolver) : FileCommandHandler(dbContext, lockService, fileQuery, authorizationService) { - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService, - IGetFileQuery fileQuery) - : base(db, mapper, authorizationService, identityResolver, lockService, fileQuery) {} - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - return await base.Handle(request, cancellationToken); - } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await AuthorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - protected override async Task PerformOperation(Domain.Models.File file) + protected override async Task PerformOperation(Domain.Models.File file, CancellationToken cancellationToken) { - var isAdmin = await _identityResolver.IsAdminAsync(); - var userId = _user.GetId(); + var canLock = await CanLock(file.Id, cancellationToken); + var userId = identityResolver.GetId(); - file.Lock(userId, isAdmin); + file.Lock(userId, canLock); try { - file.Delete(isAdmin); + file.Delete(canLock); } finally { diff --git a/src/Caster.Api/Features/Files/Requests/Edit.cs b/src/Caster.Api/Features/Files/Requests/Edit.cs index c1d910a..0e8f9c2 100644 --- a/src/Caster.Api/Features/Files/Requests/Edit.cs +++ b/src/Caster.Api/Features/Files/Requests/Edit.cs @@ -8,24 +8,21 @@ using Caster.Api.Data; using AutoMapper; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Models; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Extensions; using Caster.Api.Domain.Services; using Caster.Api.Infrastructure.Identity; -using System.Linq; -using Microsoft.EntityFrameworkCore; using Caster.Api.Features.Files.Interfaces; using System.Text.Json.Serialization; +using FluentValidation; +using Caster.Api.Features.Shared.Services; namespace Caster.Api.Features.Files { public class Edit { - [DataContract(Name="EditFileCommand")] + [DataContract(Name = "EditFileCommand")] public class Command : FileUpdateRequest, IRequest, IFileCommand { [JsonIgnore] @@ -56,49 +53,39 @@ public class Command : FileUpdateRequest, IRequest, IFileCommand public override string Content { get; set; } } - public class Handler : FileCommandHandler, IRequestHandler + public class CommandValidator : AbstractValidator + { + public CommandValidator(IValidationService validationService) + { + RuleFor(x => x.DirectoryId).DirectoryExists(validationService); + RuleFor(x => x.WorkspaceId.Value).WorkspaceExists(validationService).When(x => x.WorkspaceId.HasValue); + } + } + + public class Handler( + CasterContext dbContext, + ILockService lockService, + IGetFileQuery fileQuery, + ICasterAuthorizationService authorizationService, + IIdentityResolver identityResolver, + IMapper mapper) : FileCommandHandler(dbContext, lockService, fileQuery, authorizationService) { private Command _request { get; set; } - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService, - IGetFileQuery fileQuery) - : base(db, mapper, authorizationService, identityResolver, lockService, fileQuery) {} + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await AuthorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { _request = request; - return await base.Handle(request, cancellationToken); + return await base.HandleRequest(request, cancellationToken); } - protected override async Task PerformOperation(Domain.Models.File file) + protected override async Task PerformOperation(Domain.Models.File file, CancellationToken cancellationToken) { - await ValidateEntities(file, _request.DirectoryId, _request.WorkspaceId); - - file = _mapper.Map(_request, file); - file.Save(_user.GetId(), isAdmin: (await _identityResolver.IsAdminAsync())); + file = mapper.Map(_request, file); + file.Save(identityResolver.GetId(), canLock: await CanLock(file.Id, cancellationToken)); } - - private async Task ValidateEntities(Domain.Models.File file, Guid directoryId, Guid? workspaceId) - { - var directory = await _db.Directories.FindAsync(directoryId); - - if (directory == null) - throw new EntityNotFoundException(); - - if (workspaceId.HasValue) - { - var workspace = await _db.Workspaces.FindAsync(workspaceId.Value); - - if (workspace == null) - throw new EntityNotFoundException(); - } - } - } } } diff --git a/src/Caster.Api/Features/Files/Requests/FileCommandHandler.cs b/src/Caster.Api/Features/Files/Requests/FileCommandHandler.cs index c284ec3..6fc9ffa 100644 --- a/src/Caster.Api/Features/Files/Requests/FileCommandHandler.cs +++ b/src/Caster.Api/Features/Files/Requests/FileCommandHandler.cs @@ -4,17 +4,13 @@ using System; using System.Threading.Tasks; using Caster.Api.Data; -using AutoMapper; using Caster.Api.Infrastructure.Exceptions; -using System.Linq; -using Microsoft.EntityFrameworkCore; -using Microsoft.AspNetCore.Authorization; -using System.Security.Claims; using Caster.Api.Domain.Services; -using Caster.Api.Infrastructure.Identity; using System.Threading; +using Caster.Api.Features.Shared; +using MediatR; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Files { @@ -23,65 +19,42 @@ public interface IFileCommand Guid Id { get; set; } } - public abstract class FileCommandHandler + public abstract class FileCommandHandler( + CasterContext dbContext, + ILockService lockService, + IGetFileQuery fileQuery, + ICasterAuthorizationService authorizationService) : BaseHandler + where TRequest : IRequest, IFileCommand + where TResponse : File { - protected readonly CasterContext _db; - protected readonly IMapper _mapper; - protected readonly IAuthorizationService _authorizationService; - protected readonly ClaimsPrincipal _user; - protected readonly ILockService _lockService; - protected readonly IGetFileQuery _fileQuery; - protected readonly IIdentityResolver _identityResolver; + protected ICasterAuthorizationService AuthorizationService => authorizationService; - public FileCommandHandler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService, - IGetFileQuery fileQuery) + public override async Task HandleRequest(TRequest request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _lockService = lockService; - _fileQuery = fileQuery; - _identityResolver = identityResolver; - } - - protected async Task Handle(IFileCommand request, CancellationToken cancellationToken) - { - await this.Authorize(); - - using (var lockResult = await _lockService.GetFileLock(request.Id).LockAsync(0)) + using (var lockResult = await lockService.GetFileLock(request.Id).LockAsync(0)) { if (!lockResult.AcquiredLock) throw new FileConflictException(); - var file = await _db.Files.FindAsync(request.Id); + var file = await dbContext.Files.FindAsync(request.Id); if (file == null) throw new EntityNotFoundException(); - await this.PerformOperation(file); + await this.PerformOperation(file, cancellationToken); - await _db.SaveChangesAsync(cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); } - return await _fileQuery.ExecuteAsync(request.Id); + return (TResponse)await fileQuery.ExecuteAsync(request.Id); } - protected virtual async Task Authorize() { - if (!(await _authorizationService.AuthorizeAsync( - _user, null, new ContentDeveloperRequirement())).Succeeded) - { - throw new ForbiddenException(); - } + protected async Task CanLock(Guid fileId, CancellationToken cancellationToken) + { + return await authorizationService.Authorize(fileId, [SystemPermission.LockFiles], [ProjectPermission.LockFiles], cancellationToken); } - protected abstract Task PerformOperation(Domain.Models.File file); - + protected abstract Task PerformOperation(Domain.Models.File file, CancellationToken cancellationToken); } } diff --git a/src/Caster.Api/Features/Files/Requests/Get.cs b/src/Caster.Api/Features/Files/Requests/Get.cs index 3a52c71..52bc736 100644 --- a/src/Caster.Api/Features/Files/Requests/Get.cs +++ b/src/Caster.Api/Features/Files/Requests/Get.cs @@ -5,22 +5,17 @@ using System.Threading; using System.Threading.Tasks; using MediatR; -using AutoMapper; -using Microsoft.EntityFrameworkCore; -using AutoMapper.QueryableExtensions; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; -using Caster.Api.Data; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Files { public class Get { - [DataContract(Name="GetFileQuery")] + [DataContract(Name = "GetFileQuery")] public class Query : IRequest { /// @@ -30,28 +25,14 @@ public class Query : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IGetFileQuery fileQuery) : BaseHandler { - private readonly IGetFileQuery _fileQuery; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public Handler( - IGetFileQuery fileQuery, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _fileQuery = fileQuery; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var file = await _fileQuery.ExecuteAsync(request.Id); + var file = await fileQuery.ExecuteAsync(request.Id); if (file == null) throw new EntityNotFoundException(); diff --git a/src/Caster.Api/Features/Files/Requests/GetAll.cs b/src/Caster.Api/Features/Files/Requests/GetAll.cs index a9fb5d1..288e40a 100644 --- a/src/Caster.Api/Features/Files/Requests/GetAll.cs +++ b/src/Caster.Api/Features/Files/Requests/GetAll.cs @@ -7,21 +7,16 @@ using AutoMapper; using Caster.Api.Data; using Microsoft.EntityFrameworkCore; -using AutoMapper.QueryableExtensions; using System.Runtime.Serialization; -using System.Linq; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Files { public class GetAll { - [DataContract(Name="GetFilesQuery")] + [DataContract(Name = "GetFilesQuery")] public class Query : IRequest { /// @@ -37,36 +32,19 @@ public class Query : IRequest public bool IncludeDeleted { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewProjects], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - return await _db.Files + return await dbContext.Files .GetAll( - configurationProvider: _mapper.ConfigurationProvider, + configurationProvider: mapper.ConfigurationProvider, includeDeleted: request.IncludeDeleted, includeContent: request.IncludeContent) - .ToArrayAsync(); + .ToArrayAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Files/Requests/GetByDirectory.cs b/src/Caster.Api/Features/Files/Requests/GetByDirectory.cs index 4fdec35..7716207 100644 --- a/src/Caster.Api/Features/Files/Requests/GetByDirectory.cs +++ b/src/Caster.Api/Features/Files/Requests/GetByDirectory.cs @@ -9,19 +9,19 @@ using Caster.Api.Data; using Microsoft.EntityFrameworkCore; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Models; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using System.Text.Json.Serialization; +using Caster.Api.Features.Shared; +using FluentValidation; +using Caster.Api.Features.Shared.Services; +using Caster.Api.Infrastructure.Extensions; namespace Caster.Api.Features.Files { public class GetByDirectory { - [DataContract(Name="GetFilesByDirectoryQuery")] + [DataContract(Name = "GetFilesByDirectoryQuery")] public class Query : IRequest { [JsonIgnore] @@ -40,47 +40,28 @@ public class Query : IRequest public bool IncludeDeleted { get; set; } } - public class Handler : IRequestHandler + public class CommandValidator : AbstractValidator { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public CommandValidator(IValidationService validationService) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); + RuleFor(x => x.DirectoryId).DirectoryExists(validationService); } + } - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - await ValidateDirectory(request.DirectoryId); + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.DirectoryId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - return await _db.Files + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { + return await dbContext.Files .GetAll( - configurationProvider: _mapper.ConfigurationProvider, + configurationProvider: mapper.ConfigurationProvider, directoryId: request.DirectoryId, includeDeleted: request.IncludeDeleted, includeContent: request.IncludeContent) - .ToArrayAsync(); - } - - private async Task ValidateDirectory(Guid directoryId) - { - var directory = await _db.Directories.FindAsync(directoryId); - - if (directory == null) - throw new EntityNotFoundException(); + .ToArrayAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Files/Requests/GetFileVersion.cs b/src/Caster.Api/Features/Files/Requests/GetFileVersion.cs index c5d478c..9ae987a 100644 --- a/src/Caster.Api/Features/Files/Requests/GetFileVersion.cs +++ b/src/Caster.Api/Features/Files/Requests/GetFileVersion.cs @@ -11,17 +11,15 @@ using AutoMapper.QueryableExtensions; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Files { public class GetFileVersion { - [DataContract(Name="GetFileVersionQuery")] + [DataContract(Name = "GetFileVersionQuery")] public class Query : IRequest { /// @@ -31,32 +29,15 @@ public class Query : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal() as ClaimsPrincipal; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var fileVersion = await _db.FileVersions - .ProjectTo(_mapper.ConfigurationProvider, dest => dest.Content) + var fileVersion = await dbContext.FileVersions + .ProjectTo(mapper.ConfigurationProvider, dest => dest.Content) .SingleOrDefaultAsync(e => e.Id == request.Id, cancellationToken); if (fileVersion == null) diff --git a/src/Caster.Api/Features/Files/Requests/GetFileVersions.cs b/src/Caster.Api/Features/Files/Requests/GetFileVersions.cs index e62fe2e..07e45d4 100644 --- a/src/Caster.Api/Features/Files/Requests/GetFileVersions.cs +++ b/src/Caster.Api/Features/Files/Requests/GetFileVersions.cs @@ -11,18 +11,15 @@ using Microsoft.EntityFrameworkCore; using AutoMapper.QueryableExtensions; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Files { public class GetFileVersions { - [DataContract(Name="GetFileVersionsQuery")] + [DataContract(Name = "GetFileVersionsQuery")] public class Query : IRequest { /// @@ -32,34 +29,17 @@ public class Query : IRequest public Guid FileId { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbcontext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.FileId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal() as ClaimsPrincipal; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var fileVersions = await _db.FileVersions + var fileVersions = await dbcontext.FileVersions .Where(fv => fv.FileId == request.FileId) - .ProjectTo(_mapper.ConfigurationProvider) - .ToArrayAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToArrayAsync(cancellationToken); return fileVersions; } diff --git a/src/Caster.Api/Features/Files/Requests/Lock.cs b/src/Caster.Api/Features/Files/Requests/Lock.cs index 1134478..2402557 100644 --- a/src/Caster.Api/Features/Files/Requests/Lock.cs +++ b/src/Caster.Api/Features/Files/Requests/Lock.cs @@ -6,45 +6,38 @@ using System.Threading.Tasks; using MediatR; using Caster.Api.Data; -using AutoMapper; using System.Runtime.Serialization; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Domain.Services; using Caster.Api.Infrastructure.Identity; -using Caster.Api.Infrastructure.Extensions; using Caster.Api.Features.Files.Interfaces; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Files { public class Lock { - [DataContract(Name="LockFileCommand")] + [DataContract(Name = "LockFileCommand")] public class Command : FileMetadataUpdateRequest, IRequest, IFileCommand { public Guid Id { get; set; } } - public class Handler : FileCommandHandler, IRequestHandler + public class Handler( + CasterContext dbContext, + ILockService lockService, + IGetFileQuery fileQuery, + ICasterAuthorizationService authorizationService, + IIdentityResolver identityResolver) : FileCommandHandler(dbContext, lockService, fileQuery, authorizationService) { - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService, - IGetFileQuery fileQuery) - : base(db, mapper, authorizationService, identityResolver, lockService, fileQuery) {} - public async Task Handle(Command request, CancellationToken cancellationToken) - { - return await base.Handle(request, cancellationToken); - } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await AuthorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - protected override async Task PerformOperation(Domain.Models.File file) + protected override async Task PerformOperation(Domain.Models.File file, CancellationToken cancellationToken) { - file.Lock(_user.GetId(), isAdmin: (await _identityResolver.IsAdminAsync())); + file.Lock(identityResolver.GetId(), canLock: await CanLock(file.Id, cancellationToken)); } } } } - diff --git a/src/Caster.Api/Features/Files/Requests/PartialEdit.cs b/src/Caster.Api/Features/Files/Requests/PartialEdit.cs index 779a62f..f06ce61 100644 --- a/src/Caster.Api/Features/Files/Requests/PartialEdit.cs +++ b/src/Caster.Api/Features/Files/Requests/PartialEdit.cs @@ -8,21 +8,22 @@ using Caster.Api.Data; using AutoMapper; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Models; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Extensions; using Caster.Api.Domain.Services; using Caster.Api.Infrastructure.Identity; using Caster.Api.Features.Files.Interfaces; using CodeAnalysis = Microsoft.CodeAnalysis; using System.Text.Json.Serialization; +using Caster.Api.Infrastructure.Authorization; +using FluentValidation; +using Caster.Api.Features.Shared.Services; namespace Caster.Api.Features.Files { public class PartialEdit { - [DataContract(Name="PartialEditFileCommand")] + [DataContract(Name = "PartialEditFileCommand")] public class Command : FileUpdateRequest, IRequest, IFileCommand { [JsonIgnore] @@ -54,51 +55,38 @@ public class Command : FileUpdateRequest, IRequest, IFileCommand public override string Content { get; set; } } - public class Handler : FileCommandHandler, IRequestHandler + public class CommandValidator : AbstractValidator { - private Command _request { get; set; } + public CommandValidator(IValidationService validationService) + { + RuleFor(x => x.DirectoryId.Value).DirectoryExists(validationService).When(x => x.DirectoryId.HasValue); + RuleFor(x => x.WorkspaceId.Value.Value).WorkspaceExists(validationService).When(x => x.WorkspaceId.HasValue && x.WorkspaceId.Value.HasValue); + } + } - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService, - IGetFileQuery fileQuery) - : base(db, mapper, authorizationService, identityResolver, lockService, fileQuery) {} + public class Handler( + CasterContext dbContext, + ILockService lockService, + IGetFileQuery fileQuery, + ICasterAuthorizationService authorizationService, + IIdentityResolver identityResolver, + IMapper mapper) : FileCommandHandler(dbContext, lockService, fileQuery, authorizationService) + { + private Command _request { get; set; } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await AuthorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { _request = request; - return await base.Handle(request, cancellationToken); + return await base.HandleRequest(request, cancellationToken); } - protected override async Task PerformOperation(Domain.Models.File file) + protected override async Task PerformOperation(Domain.Models.File file, CancellationToken cancellationToken) { - await ValidateEntities(file, _request.DirectoryId, _request.WorkspaceId); - - file = _mapper.Map(_request, file); - file.Save(_user.GetId(), isAdmin: (await _identityResolver.IsAdminAsync())); - } - - private async Task ValidateEntities(Domain.Models.File file, Guid? directoryId, CodeAnalysis.Optional workspaceId) - { - if (directoryId.HasValue) - { - var directory = await _db.Directories.FindAsync(directoryId); - - if (directory == null) - throw new EntityNotFoundException(); - } - - if (workspaceId.HasValue && workspaceId.Value.HasValue) - { - var workspace = await _db.Workspaces.FindAsync(workspaceId.Value.Value); - - if (workspace == null) - throw new EntityNotFoundException(); - } + file = mapper.Map(_request, file); + file.Save(identityResolver.GetId(), canLock: await CanLock(file.Id, cancellationToken)); } } } diff --git a/src/Caster.Api/Features/Files/Requests/Rename.cs b/src/Caster.Api/Features/Files/Requests/Rename.cs index f9701a1..02e7d83 100644 --- a/src/Caster.Api/Features/Files/Requests/Rename.cs +++ b/src/Caster.Api/Features/Files/Requests/Rename.cs @@ -6,22 +6,20 @@ using System.Threading.Tasks; using MediatR; using Caster.Api.Data; -using AutoMapper; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Models; -using Microsoft.AspNetCore.Authorization; -using Caster.Api.Infrastructure.Extensions; using Caster.Api.Domain.Services; using Caster.Api.Infrastructure.Identity; using Caster.Api.Features.Files.Interfaces; using System.Text.Json.Serialization; +using Caster.Api.Infrastructure.Authorization; namespace Caster.Api.Features.Files { public class Rename { - [DataContract(Name="RenameFileCommand")] + [DataContract(Name = "RenameFileCommand")] public class Command : FileUpdateRequest, IRequest, IFileCommand { [JsonIgnore] @@ -37,51 +35,50 @@ public class Command : FileUpdateRequest, IRequest, IFileCommand public override string Content { get; set; } } - public class Handler : FileCommandHandler, IRequestHandler + public class Handler( + CasterContext dbContext, + ILockService lockService, + IGetFileQuery fileQuery, + ICasterAuthorizationService authorizationService, + IIdentityResolver identityResolver) : FileCommandHandler(dbContext, lockService, fileQuery, authorizationService) { private Command _request { get; set; } - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService, - IGetFileQuery fileQuery) - : base(db, mapper, authorizationService, identityResolver, lockService, fileQuery) {} + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await AuthorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { _request = request; - return await base.Handle(request, cancellationToken); + return await base.HandleRequest(request, cancellationToken); } - protected override async Task PerformOperation(Domain.Models.File file) + protected override async Task PerformOperation(Domain.Models.File file, CancellationToken cancellationToken) { - var isAdmin = await _identityResolver.IsAdminAsync(); - var userId = _user.GetId(); - var isNotAlreadyLocked = (userId != file.LockedById); + var canLock = await CanLock(file.Id, cancellationToken); + var userId = identityResolver.GetId(); + var isNotAlreadyLocked = userId != file.LockedById; - if(isNotAlreadyLocked) + if (isNotAlreadyLocked) { try { - file.Lock(userId, isAdmin); + file.Lock(userId, canLock); } - catch (FileConflictException) + catch (FileConflictException) { - throw new FileConflictException ("Cannot rename a file while it's being edited or locked by another user."); + throw new FileConflictException("Cannot rename a file while it's being edited or locked by another user."); } } try { file.Name = _request.Name; - file.Save(userId, isAdmin); + file.Save(userId, canLock); } finally { - if(isNotAlreadyLocked) + if (isNotAlreadyLocked) { file.Unlock(userId); } diff --git a/src/Caster.Api/Features/Files/Requests/Tag.cs b/src/Caster.Api/Features/Files/Requests/Tag.cs index e1c2d15..7db1761 100644 --- a/src/Caster.Api/Features/Files/Requests/Tag.cs +++ b/src/Caster.Api/Features/Files/Requests/Tag.cs @@ -12,18 +12,17 @@ using AutoMapper.QueryableExtensions; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Extensions; using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; +using System.Collections.Generic; namespace Caster.Api.Features.Files { public class Tag { - [DataContract(Name="TagFileCommand")] + [DataContract(Name = "TagFileCommand")] public class Command : IRequest { /// @@ -40,34 +39,32 @@ public class Command : IRequest } - public class Handler : IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + IIdentityResolver identityResolver) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task Authorize(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal() as ClaimsPrincipal; + List> authTasks = []; + + foreach (var fileId in request.FileIds) + { + authTasks.Add(authorizationService.Authorize(fileId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken)); + } + + await Task.WhenAll(authTasks); + + return authTasks.Any(x => !x.Result); } - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - var dateTagged = DateTime.UtcNow; var tag = request.Tag; - var files = await _db.Files + var files = await dbContext.Files .Where(f => request.FileIds.Contains(f.Id)) .ToArrayAsync(); @@ -77,14 +74,14 @@ public async Task Handle(Command request, CancellationToken cance if (file == null) throw new EntityNotFoundException($"File {fileId} could not be found."); - file.Tag(tag, _user.GetId(), dateTagged); + file.Tag(tag, identityResolver.GetId(), dateTagged); } - await _db.SaveChangesAsync(cancellationToken); - return await _db.FileVersions + await dbContext.SaveChangesAsync(cancellationToken); + return await dbContext.FileVersions .Where(fileVersion => fileVersion.Tag == request.Tag) - .ProjectTo(_mapper.ConfigurationProvider) - .ToArrayAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToArrayAsync(cancellationToken); } } diff --git a/src/Caster.Api/Features/Files/Requests/Unlock.cs b/src/Caster.Api/Features/Files/Requests/Unlock.cs index dbf24a0..32eca3a 100644 --- a/src/Caster.Api/Features/Files/Requests/Unlock.cs +++ b/src/Caster.Api/Features/Files/Requests/Unlock.cs @@ -18,38 +18,31 @@ using Caster.Api.Infrastructure.Identity; using Caster.Api.Infrastructure.Extensions; using Caster.Api.Features.Files.Interfaces; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Files { public class Unlock { - [DataContract(Name="UnlockFileCommand")] + [DataContract(Name = "UnlockFileCommand")] public class Command : FileMetadataUpdateRequest, IRequest, IFileCommand { public Guid Id { get; set; } } - public class Handler : FileCommandHandler, IRequestHandler + public class Handler( + CasterContext dbContext, + ILockService lockService, + IGetFileQuery fileQuery, + ICasterAuthorizationService authorizationService, + IIdentityResolver identityResolver) : FileCommandHandler(dbContext, lockService, fileQuery, authorizationService) { - private Command _request { get; set; } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await AuthorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService, - IGetFileQuery fileQuery) - : base(db, mapper, authorizationService, identityResolver, lockService, fileQuery) {} - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - return await base.Handle(request, cancellationToken); - } - - protected override Task PerformOperation(Domain.Models.File file) + protected override Task PerformOperation(Domain.Models.File file, CancellationToken cancellationToken) { - file.Unlock(_user.GetId()); + file.Unlock(identityResolver.GetId()); return Task.CompletedTask; } } diff --git a/src/Caster.Api/Features/Groups/EventHandlers/AuthCacheEventHandler.cs b/src/Caster.Api/Features/Groups/EventHandlers/AuthCacheEventHandler.cs new file mode 100644 index 0000000..1278a4e --- /dev/null +++ b/src/Caster.Api/Features/Groups/EventHandlers/AuthCacheEventHandler.cs @@ -0,0 +1,40 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Caster.Api.Domain.Events; +using MediatR; +using Microsoft.Extensions.Caching.Memory; + +namespace Caster.Api.Features.Groups.EventHandlers; + +public class GroupMembershipCreatedAuthCacheHandler(IMemoryCache cache) : + INotificationHandler> +{ + public Task Handle(EntityCreated notification, CancellationToken cancellationToken) + { + cache.Remove(notification.Entity.UserId); + return Task.CompletedTask; + } +} + +public class GroupMembershipUpdatedAuthCacheHandler(IMemoryCache cache) : + INotificationHandler> +{ + public Task Handle(EntityUpdated notification, CancellationToken cancellationToken) + { + cache.Remove(notification.Entity.UserId); + return Task.CompletedTask; + } +} + +public class GroupMembershipDeletedAuthCacheHandler(IMemoryCache cache) : + INotificationHandler> +{ + public Task Handle(EntityDeleted notification, CancellationToken cancellationToken) + { + cache.Remove(notification.Entity.UserId); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Caster.Api/Features/Groups/EventHandlers/SignalREventHandler.cs b/src/Caster.Api/Features/Groups/EventHandlers/SignalREventHandler.cs new file mode 100644 index 0000000..37c5f93 --- /dev/null +++ b/src/Caster.Api/Features/Groups/EventHandlers/SignalREventHandler.cs @@ -0,0 +1,58 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Caster.Api.Data; +using Caster.Api.Domain.Events; +using Caster.Api.Hubs; +using MediatR; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; + +namespace Caster.Api.Features.Groups.EventHandlers; + +public class GroupMembershipCreatedSignalRHandler(CasterContext _db, IMapper _mapper, IHubContext _projectHub) : + GroupMembershipBaseSignalRHandler(_db, _mapper, _projectHub), + INotificationHandler> +{ + public async Task Handle(EntityCreated notification, CancellationToken cancellationToken) + { + await base.Handle(notification.Entity, ProjectHubMethods.GroupMembershipCreated, null, cancellationToken); + } +} + +public class GroupMembershipUpdatedSignalRHandler(CasterContext _db, IMapper _mapper, IHubContext _projectHub) : + GroupMembershipBaseSignalRHandler(_db, _mapper, _projectHub), + INotificationHandler> +{ + public async Task Handle(EntityUpdated notification, CancellationToken cancellationToken) + { + await base.Handle(notification.Entity, ProjectHubMethods.GroupMembershipUpdated, notification.ModifiedProperties, cancellationToken); + } +} + +public class GroupMembershipDeletedSignalRHandler(IHubContext projectHub) : + INotificationHandler> +{ + public async Task Handle(EntityDeleted notification, CancellationToken cancellationToken) + { + await projectHub.Clients.Group(notification.Entity.GroupId.ToString()).SendAsync(ProjectHubMethods.GroupMembershipDeleted, notification.Entity.Id); + } +} + +public class GroupMembershipBaseSignalRHandler(CasterContext db, IMapper mapper, IHubContext projectHub) +{ + protected async Task Handle(Domain.Models.GroupMembership entity, string method, string[] modifiedProperties, CancellationToken cancellationToken) + { + var groupMembership = await db.GroupMemberships + .Where(x => x.Id == entity.Id) + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(); + + await projectHub.Clients.Group(entity.GroupId.ToString()).SendAsync(method, groupMembership, modifiedProperties, cancellationToken); + } +} diff --git a/src/Caster.Api/Features/Groups/Group.cs b/src/Caster.Api/Features/Groups/Group.cs new file mode 100644 index 0000000..c04f334 --- /dev/null +++ b/src/Caster.Api/Features/Groups/Group.cs @@ -0,0 +1,20 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; + +namespace Caster.Api.Features.Groups +{ + public class Group + { + /// + /// ID of the group. + /// + public Guid Id { get; set; } + + /// + /// Name of the group. + /// + public string Name { get; set; } + } +} diff --git a/src/Caster.Api/Features/UserPermissions/UserPermission.cs b/src/Caster.Api/Features/Groups/GroupMembership.cs similarity index 52% rename from src/Caster.Api/Features/UserPermissions/UserPermission.cs rename to src/Caster.Api/Features/Groups/GroupMembership.cs index f71251d..351ed65 100644 --- a/src/Caster.Api/Features/UserPermissions/UserPermission.cs +++ b/src/Caster.Api/Features/Groups/GroupMembership.cs @@ -3,13 +3,20 @@ using System; -namespace Caster.Api.Features.UserPermissions +namespace Caster.Api.Features.Groups { - public class UserPermission + public class GroupMembership { public Guid Id { get; set; } + + /// + /// ID of the group. + /// + public Guid GroupId { get; set; } + + /// + /// Id of the User. + /// public Guid UserId { get; set; } - public Guid PermissionId { get; set; } } } - diff --git a/src/Caster.Api/Features/Groups/GroupsController.cs b/src/Caster.Api/Features/Groups/GroupsController.cs new file mode 100644 index 0000000..dcccfb7 --- /dev/null +++ b/src/Caster.Api/Features/Groups/GroupsController.cs @@ -0,0 +1,151 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis; +using Swashbuckle.AspNetCore.Annotations; + +namespace Caster.Api.Features.Groups +{ + [Route("api/groups")] + [ApiController] + [Authorize] + public class GroupsController : ControllerBase + { + private readonly IMediator _mediator; + + public GroupsController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// Get a single group. + /// + /// ID of an group. + /// + [HttpGet("{id}")] + [ProducesResponseType(typeof(Group), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "GetGroup")] + public async Task Get([FromRoute] Guid id) + { + var result = await _mediator.Send(new Get.Query { Id = id }); + return Ok(result); + } + + /// + /// Get all groups. + /// + /// + [HttpGet()] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "GetAllGroups")] + public async Task GetAll() + { + var result = await _mediator.Send(new GetAll.Query()); + return Ok(result); + } + + /// + /// Create a new group. + /// + /// + /// + [HttpPost()] + [ProducesResponseType(typeof(Group), (int)HttpStatusCode.Created)] + [SwaggerOperation(OperationId = "CreateGroup")] + public async Task Create(Create.Command command) + { + var result = await _mediator.Send(command); + return CreatedAtAction(nameof(Get), new { id = result.Id }, result); + } + + /// + /// Update a group. + /// + /// + [HttpPut("")] + [ProducesResponseType(typeof(Group), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "EditGroup")] + public async Task Edit(Edit.Command command) + { + var result = await _mediator.Send(command); + return Ok(result); + } + + /// + /// Delete a group. + /// + /// ID of an group. + /// + [HttpDelete("{id}")] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + [SwaggerOperation(OperationId = "DeleteGroup")] + public async Task Delete([FromRoute] Guid id) + { + await _mediator.Send(new Delete.Command { Id = id }); + return NoContent(); + } + + /// + /// Get a single Group Membership. + /// + /// + [HttpGet("membership/{id}")] + [ProducesResponseType(typeof(GroupMembership), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "GetGroupMembership")] + public async Task GetMembership([FromRoute] Guid id) + { + var result = await _mediator.Send(new GetMembership.Query { Id = id }); + return Ok(result); + } + + /// + /// Get all Group Memberships of a Group. + /// + /// + [HttpGet("{groupId}/memberships")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "GetGroupMemberships")] + public async Task GetMemberships([FromRoute] Guid groupId) + { + var result = await _mediator.Send(new GetMemberships.Query() { GroupId = groupId }); + return Ok(result); + } + + /// + /// Create a new Group Membership. + /// + /// + /// + /// + [HttpPost("{groupId}/memberships")] + [ProducesResponseType(typeof(GroupMembership), (int)HttpStatusCode.Created)] + [SwaggerOperation(OperationId = "CreateGroupMembership")] + public async Task CreateMembership([FromRoute] Guid groupId, CreateMembership.Command command) + { + command.GroupId = groupId; + var result = await _mediator.Send(command); + return CreatedAtAction(nameof(Get), new { id = result.Id }, result); + } + + /// + /// Delete a Group Membership. + /// + /// + [HttpDelete("memberships/{id}")] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + [SwaggerOperation(OperationId = "DeleteGroupMembership")] + public async Task DeleteMembership([FromRoute] Guid id) + { + await _mediator.Send(new DeleteMembership.Command { Id = id }); + return NoContent(); + } + } +} diff --git a/src/Caster.Api/Features/Groups/MappingProfile.cs b/src/Caster.Api/Features/Groups/MappingProfile.cs new file mode 100644 index 0000000..3785a25 --- /dev/null +++ b/src/Caster.Api/Features/Groups/MappingProfile.cs @@ -0,0 +1,21 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +namespace Caster.Api.Features.Groups +{ + using System.Linq; + using AutoMapper; + using Caster.Api.Domain.Models; + + public class MappingProfile : Profile + { + public MappingProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + } + } +} diff --git a/src/Caster.Api/Features/Groups/Requests/Create.cs b/src/Caster.Api/Features/Groups/Requests/Create.cs new file mode 100644 index 0000000..a6403b0 --- /dev/null +++ b/src/Caster.Api/Features/Groups/Requests/Create.cs @@ -0,0 +1,43 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using AutoMapper; +using Caster.Api.Data; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Domain.Models; +using Caster.Api.Features.Shared; + +namespace Caster.Api.Features.Groups +{ + public class Create + { + [DataContract(Name = "CreateGroupCommand")] + public class Command : IRequest + { + /// + /// Name of the group. + /// + [DataMember] + public string Name { get; set; } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageGroups], cancellationToken); + + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) + { + var group = mapper.Map(request); + dbContext.Groups.Add(group); + + await dbContext.SaveChangesAsync(); + return mapper.Map(group); + } + } + } +} diff --git a/src/Caster.Api/Features/Groups/Requests/CreateMembership.cs b/src/Caster.Api/Features/Groups/Requests/CreateMembership.cs new file mode 100644 index 0000000..58dd169 --- /dev/null +++ b/src/Caster.Api/Features/Groups/Requests/CreateMembership.cs @@ -0,0 +1,71 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Caster.Api.Data; +using AutoMapper; +using System.Runtime.Serialization; +using Caster.Api.Infrastructure.Exceptions; +using Microsoft.EntityFrameworkCore; +using FluentValidation; +using Caster.Api.Features.Shared.Services; +using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Domain.Models; +using System.Text.Json.Serialization; +using Caster.Api.Features.Shared; +using Caster.Api.Infrastructure.Authorization; + +namespace Caster.Api.Features.Groups +{ + public class CreateMembership + { + [DataContract(Name = "CreateGroupMembershipCommand")] + public record Command : IRequest + { + /// + /// The Id of the Group to add to. + /// + [JsonIgnore] + public Guid GroupId { get; set; } + + /// + /// The Id of the User to add. + /// + [DataMember] + public Guid UserId { get; set; } + } + + public class Validator : AbstractValidator + { + public Validator(IValidationService validationService) + { + RuleFor(x => x.GroupId).GroupExists(validationService); + RuleFor(x => x.UserId).UserExists(validationService); + } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageGroups], cancellationToken); + + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) + { + var groupMembershipExists = await dbContext.GroupMemberships + .AnyAsync(x => x.GroupId == request.GroupId && x.UserId == request.UserId, cancellationToken); + + if (groupMembershipExists) + throw new ConflictException("User is already a member of this Group"); + + var groupMembership = new Domain.Models.GroupMembership(request.GroupId, request.UserId); + dbContext.GroupMemberships.Add(groupMembership); + await dbContext.SaveChangesAsync(); + + return mapper.Map(groupMembership); + } + } + } +} diff --git a/src/Caster.Api/Features/Groups/Requests/Delete.cs b/src/Caster.Api/Features/Groups/Requests/Delete.cs new file mode 100644 index 0000000..ee6769a --- /dev/null +++ b/src/Caster.Api/Features/Groups/Requests/Delete.cs @@ -0,0 +1,43 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Caster.Api.Data; +using Caster.Api.Infrastructure.Exceptions; +using System.Runtime.Serialization; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Features.Groups +{ + public class Delete + { + [DataContract(Name = "DeleteGroupCommand")] + public class Command : IRequest + { + public Guid Id { get; set; } + } + + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageGroups], cancellationToken); + + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) + { + var entry = dbContext.Groups.FirstOrDefault(e => e.Id == request.Id); + + if (entry == null) + throw new EntityNotFoundException(); + + dbContext.Groups.Remove(entry); + await dbContext.SaveChangesAsync(cancellationToken); + } + } + } +} diff --git a/src/Caster.Api/Features/Groups/Requests/DeleteMembership.cs b/src/Caster.Api/Features/Groups/Requests/DeleteMembership.cs new file mode 100644 index 0000000..26c77e8 --- /dev/null +++ b/src/Caster.Api/Features/Groups/Requests/DeleteMembership.cs @@ -0,0 +1,52 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Caster.Api.Data; +using System.Runtime.Serialization; +using Microsoft.EntityFrameworkCore; +using FluentValidation; +using Caster.Api.Features.Shared.Services; +using Caster.Api.Infrastructure.Extensions; +using System.Linq; +using System.Text.Json.Serialization; +using Caster.Api.Infrastructure.Exceptions; +using Caster.Api.Features.Shared; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Features.Groups +{ + public class DeleteMembership + { + [DataContract(Name = "DeleteGroupMembershipCommand")] + public record Command : IRequest + { + /// + /// The Id of the Group Membership to delete + /// + [JsonIgnore] + public Guid Id { get; set; } + } + + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageGroups], cancellationToken); + + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) + { + var groupMembership = await dbContext.GroupMemberships.FindAsync([request.Id], cancellationToken); + + if (groupMembership == null) + throw new EntityNotFoundException(); + + dbContext.GroupMemberships.Remove(groupMembership); + await dbContext.SaveChangesAsync(); + } + } + } +} diff --git a/src/Caster.Api/Features/Groups/Requests/Edit.cs b/src/Caster.Api/Features/Groups/Requests/Edit.cs new file mode 100644 index 0000000..19b6a9d --- /dev/null +++ b/src/Caster.Api/Features/Groups/Requests/Edit.cs @@ -0,0 +1,54 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Caster.Api.Data; +using AutoMapper; +using System.Runtime.Serialization; +using Caster.Api.Infrastructure.Exceptions; +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Features.Groups +{ + public class Edit + { + [DataContract(Name = "EditGroupCommand")] + public class Command : IRequest + { + [DataMember] + public Guid Id { get; set; } + + /// + /// Name of the group. + /// + [DataMember] + public string Name { get; set; } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageGroups], cancellationToken); + + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) + { + var group = await dbContext.Groups.FindAsync([request.Id], cancellationToken); + + if (group == null) + throw new EntityNotFoundException(); + + mapper.Map(request, group); + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(group); + } + } + } +} diff --git a/src/Caster.Api/Features/Groups/Requests/Get.cs b/src/Caster.Api/Features/Groups/Requests/Get.cs new file mode 100644 index 0000000..0a2b13f --- /dev/null +++ b/src/Caster.Api/Features/Groups/Requests/Get.cs @@ -0,0 +1,50 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using AutoMapper.QueryableExtensions; +using System.Runtime.Serialization; +using Caster.Api.Data; +using Caster.Api.Infrastructure.Exceptions; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Features.Groups +{ + public class Get + { + [DataContract(Name = "GetGroupQuery")] + public class Query : IRequest + { + /// + /// The Id of the Group to retrieve + /// + [DataMember] + public Guid Id { get; set; } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewGroups], cancellationToken); + + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { + var group = await dbContext.Groups + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(e => e.Id == request.Id); + + if (group == null) + throw new EntityNotFoundException(); + + return group; + } + } + } +} diff --git a/src/Caster.Api/Features/Groups/Requests/GetAll.cs b/src/Caster.Api/Features/Groups/Requests/GetAll.cs new file mode 100644 index 0000000..56e65c9 --- /dev/null +++ b/src/Caster.Api/Features/Groups/Requests/GetAll.cs @@ -0,0 +1,48 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using AutoMapper.QueryableExtensions; +using Caster.Api.Data; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; +using System.Linq; + +namespace Caster.Api.Features.Groups +{ + public class GetAll + { + [DataContract(Name = "GetGroupsQuery")] + public class Query : IRequest + { + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) + { + if (await authorizationService.Authorize([SystemPermission.ViewGroups, SystemPermission.ViewProjects], cancellationToken)) + { + return true; + } + + return authorizationService. + GetProjectPermissions() + .Any(x => x.Permissions.Contains(ProjectPermission.ManageProject)); + } + + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { + return await dbContext.Groups + .ProjectTo(mapper.ConfigurationProvider) + .ToArrayAsync(cancellationToken); + } + } + } +} diff --git a/src/Caster.Api/Features/Groups/Requests/GetMembership.cs b/src/Caster.Api/Features/Groups/Requests/GetMembership.cs new file mode 100644 index 0000000..a444fdc --- /dev/null +++ b/src/Caster.Api/Features/Groups/Requests/GetMembership.cs @@ -0,0 +1,54 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Caster.Api.Data; +using AutoMapper; +using System.Runtime.Serialization; +using Caster.Api.Infrastructure.Exceptions; +using Microsoft.EntityFrameworkCore; +using AutoMapper.QueryableExtensions; +using Caster.Api.Infrastructure.Authorization; +using FluentValidation; +using System.Linq; +using System.Text.Json.Serialization; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Features.Groups +{ + public class GetMembership + { + [DataContract(Name = "GetGroupMembershipQuery")] + public record Query : IRequest + { + /// + /// Id of the GroupMembership. + /// + [JsonIgnore] + public Guid Id { get; set; } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewGroups], cancellationToken); + + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { + var groupMembership = await dbContext.GroupMemberships + .Where(x => x.Id == request.Id) + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(cancellationToken); + + if (groupMembership == null) + throw new EntityNotFoundException(); + + return groupMembership; + } + } + } +} diff --git a/src/Caster.Api/Features/Groups/Requests/GetMemberships.cs b/src/Caster.Api/Features/Groups/Requests/GetMemberships.cs new file mode 100644 index 0000000..7eefc8c --- /dev/null +++ b/src/Caster.Api/Features/Groups/Requests/GetMemberships.cs @@ -0,0 +1,53 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using AutoMapper.QueryableExtensions; +using Caster.Api.Data; +using Caster.Api.Infrastructure.Authorization; +using System; +using FluentValidation; +using Caster.Api.Features.Shared.Services; +using Caster.Api.Infrastructure.Extensions; +using System.Linq; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Features.Groups +{ + public class GetMemberships + { + [DataContract(Name = "GetGroupMembershipsQuery")] + public record Query : IRequest + { + public Guid GroupId { get; set; } + } + + public class Validator : AbstractValidator + { + public Validator(IValidationService validationService) + { + RuleFor(x => x.GroupId).GroupExists(validationService); + } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewGroups], cancellationToken); + + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { + return await dbContext.GroupMemberships + .Where(x => x.GroupId == request.GroupId) + .ProjectTo(mapper.ConfigurationProvider) + .ToArrayAsync(); + } + } + } +} diff --git a/src/Caster.Api/Features/Hosts/Requests/Create.cs b/src/Caster.Api/Features/Hosts/Requests/Create.cs index 218c09e..0127f23 100644 --- a/src/Caster.Api/Features/Hosts/Requests/Create.cs +++ b/src/Caster.Api/Features/Hosts/Requests/Create.cs @@ -8,18 +8,15 @@ using AutoMapper; using Caster.Api.Data; using System; -using Microsoft.AspNetCore.Authorization; -using System.Security.Principal; -using System.Security.Claims; -using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Hosts { public class Create { - [DataContract(Name="CreateHostCommand")] + [DataContract(Name = "CreateHostCommand")] public class Command : IRequest { /// @@ -59,30 +56,17 @@ public class Command : IRequest public Guid? ProjectId { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageHosts], cancellationToken); - public Handler(CasterContext db, IMapper mapper, IAuthorizationService authorizationService, IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var Host = _mapper.Map(request); - await _db.Hosts.AddAsync(Host); - await _db.SaveChangesAsync(); - return _mapper.Map(Host); + var host = mapper.Map(request); + dbContext.Hosts.Add(host); + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(host); } } } diff --git a/src/Caster.Api/Features/Hosts/Requests/Delete.cs b/src/Caster.Api/Features/Hosts/Requests/Delete.cs index 7a86160..6e31ff2 100644 --- a/src/Caster.Api/Features/Hosts/Requests/Delete.cs +++ b/src/Caster.Api/Features/Hosts/Requests/Delete.cs @@ -2,19 +2,15 @@ // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; using MediatR; -using AutoMapper; using Caster.Api.Data; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using Microsoft.AspNetCore.Authorization; -using System.Security.Claims; -using System.Security.Principal; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Hosts { @@ -26,31 +22,20 @@ public class Command : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageHosts], cancellationToken); - public Handler(CasterContext db, IAuthorizationService authorizationService, IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var entry = _db.Hosts.FirstOrDefault(e => e.Id == request.Id); + var entry = await dbContext.Hosts.FindAsync([request.Id], cancellationToken); if (entry == null) throw new EntityNotFoundException(); - _db.Hosts.Remove(entry); - await _db.SaveChangesAsync(cancellationToken); + dbContext.Hosts.Remove(entry); + await dbContext.SaveChangesAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Hosts/Requests/Edit.cs b/src/Caster.Api/Features/Hosts/Requests/Edit.cs index 40e1223..5fac4f8 100644 --- a/src/Caster.Api/Features/Hosts/Requests/Edit.cs +++ b/src/Caster.Api/Features/Hosts/Requests/Edit.cs @@ -9,22 +9,20 @@ using AutoMapper; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using Microsoft.AspNetCore.Authorization; -using System.Security.Claims; -using System.Security.Principal; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Hosts { public class Edit { - [DataContract(Name="EditHostCommand")] + [DataContract(Name = "EditHostCommand")] public class Command : IRequest { public Guid Id { get; set; } - /// + /// /// Name of the Host /// [DataMember] @@ -61,34 +59,21 @@ public class Command : IRequest public Guid? ProjectId { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageHosts], cancellationToken); - public Handler(CasterContext db, IMapper mapper, IAuthorizationService authorizationService, IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var host = await _db.Hosts.FindAsync(request.Id); + var host = await dbContext.Hosts.FindAsync([request.Id], cancellationToken); if (host == null) throw new EntityNotFoundException(); - _mapper.Map(request, host); - await _db.SaveChangesAsync(); - return _mapper.Map(host); + mapper.Map(request, host); + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(host); } } } diff --git a/src/Caster.Api/Features/Hosts/Requests/Get.cs b/src/Caster.Api/Features/Hosts/Requests/Get.cs index 3c9b17f..b09c860 100644 --- a/src/Caster.Api/Features/Hosts/Requests/Get.cs +++ b/src/Caster.Api/Features/Hosts/Requests/Get.cs @@ -11,17 +11,15 @@ using System.Runtime.Serialization; using Caster.Api.Data; using Caster.Api.Infrastructure.Exceptions; -using Microsoft.AspNetCore.Authorization; -using System.Security.Claims; -using System.Security.Principal; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Hosts { public class Get { - [DataContract(Name="GetHostQuery")] + [DataContract(Name = "GetHostQuery")] public class Query : IRequest { /// @@ -31,34 +29,21 @@ public class Query : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewHosts], cancellationToken); - public Handler(CasterContext db, IMapper mapper, IAuthorizationService authorizationService, IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var Host = await _db.Hosts - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(e => e.Id == request.Id); + var host = await dbContext.Hosts + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(e => e.Id == request.Id, cancellationToken); - if (Host == null) + if (host == null) throw new EntityNotFoundException(); - return Host; + return host; } } } diff --git a/src/Caster.Api/Features/Hosts/Requests/GetAll.cs b/src/Caster.Api/Features/Hosts/Requests/GetAll.cs index fac1978..3424675 100644 --- a/src/Caster.Api/Features/Hosts/Requests/GetAll.cs +++ b/src/Caster.Api/Features/Hosts/Requests/GetAll.cs @@ -9,46 +9,29 @@ using AutoMapper.QueryableExtensions; using System.Runtime.Serialization; using Caster.Api.Data; -using System; -using Microsoft.AspNetCore.Authorization; -using System.Security.Claims; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using System.Security.Principal; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Hosts { public class GetAll { - [DataContract(Name="GetHostsQuery")] + [DataContract(Name = "GetHostsQuery")] public class Query : IRequest { } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewHosts], cancellationToken); - public Handler(CasterContext db, IMapper mapper, IAuthorizationService authorizationService, IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - return await _db.Hosts - .ProjectTo(_mapper.ConfigurationProvider) - .ToArrayAsync(); + return await dbContext.Hosts + .ProjectTo(mapper.ConfigurationProvider) + .ToArrayAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Hosts/Requests/PartialEdit.cs b/src/Caster.Api/Features/Hosts/Requests/PartialEdit.cs index 27d4a50..b932f76 100644 --- a/src/Caster.Api/Features/Hosts/Requests/PartialEdit.cs +++ b/src/Caster.Api/Features/Hosts/Requests/PartialEdit.cs @@ -16,12 +16,13 @@ using Caster.Api.Infrastructure.Authorization; using System.Security.Principal; using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Hosts { public class PartialEdit { - [DataContract(Name="PartialEditHostCommand")] + [DataContract(Name = "PartialEditHostCommand")] public class Command : IRequest { public Guid Id { get; set; } @@ -63,34 +64,21 @@ public class Command : IRequest public Optional ProjectId { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageHosts], cancellationToken); - public Handler(CasterContext db, IMapper mapper, IAuthorizationService authorizationService, IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var host = await _db.Hosts.FindAsync(request.Id); + var host = await dbContext.Hosts.FindAsync([request.Id], cancellationToken); if (host == null) throw new EntityNotFoundException(); - _mapper.Map(request, host); - await _db.SaveChangesAsync(); - return _mapper.Map(host); + mapper.Map(request, host); + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(host); } } } diff --git a/src/Caster.Api/Features/Modules/Requests/Create.cs b/src/Caster.Api/Features/Modules/Requests/Create.cs index 8006ee9..d86d7d9 100644 --- a/src/Caster.Api/Features/Modules/Requests/Create.cs +++ b/src/Caster.Api/Features/Modules/Requests/Create.cs @@ -1,7 +1,6 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -9,18 +8,15 @@ using Caster.Api.Data; using AutoMapper; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Modules { public class Create { - [DataContract(Name="CreateModuleCommand")] + [DataContract(Name = "CreateModuleCommand")] public class Command : IRequest { /// @@ -65,37 +61,19 @@ public class Command : IRequest public List Outputs { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageModules], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); + var module = mapper.Map(request); - var module = _mapper.Map(request); - - await _db.Modules.AddAsync(module, cancellationToken); - await _db.SaveChangesAsync(cancellationToken); - return _mapper.Map(module); + dbContext.Modules.Add(module); + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(module); } - } } } diff --git a/src/Caster.Api/Features/Modules/Requests/CreateFromRepository.cs b/src/Caster.Api/Features/Modules/Requests/CreateFromRepository.cs index 0fa0e64..9874e05 100644 --- a/src/Caster.Api/Features/Modules/Requests/CreateFromRepository.cs +++ b/src/Caster.Api/Features/Modules/Requests/CreateFromRepository.cs @@ -3,23 +3,18 @@ using System.Threading; using System.Threading.Tasks; -using AutoMapper; using MediatR; -using Caster.Api.Data; using System.Runtime.Serialization; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Services; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Modules { public class CreateFromRepository { - [DataContract(Name="CreateModuleRepositoryCommand")] + [DataContract(Name = "CreateModuleRepositoryCommand")] public class Command : IRequest { /// @@ -29,37 +24,18 @@ public class Command : IRequest public string Id { get; set; } } - public class Handler : IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IGitlabRepositoryService gitlabRepositoryService) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly IGitlabRepositoryService _gitlabRepositoryService; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageModules], cancellationToken); - public Handler(CasterContext db, IMapper mapper, - IGitlabRepositoryService gitlabRepositoryService, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _gitlabRepositoryService = gitlabRepositoryService; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - // TODO: add handling for other repositories? - var isSuccess = await _gitlabRepositoryService.GetModuleAsync(request.Id, cancellationToken); - - return isSuccess; + return await gitlabRepositoryService.GetModuleAsync(request.Id, cancellationToken); } - } } } diff --git a/src/Caster.Api/Features/Modules/Requests/CreateSnippet.cs b/src/Caster.Api/Features/Modules/Requests/CreateSnippet.cs index c0d9524..0dbaff6 100644 --- a/src/Caster.Api/Features/Modules/Requests/CreateSnippet.cs +++ b/src/Caster.Api/Features/Modules/Requests/CreateSnippet.cs @@ -8,16 +8,12 @@ using Microsoft.EntityFrameworkCore; using MediatR; using Caster.Api.Data; -using AutoMapper; using System.Runtime.Serialization; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using System.Linq; using Caster.Api.Domain.Models; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Modules { @@ -46,31 +42,14 @@ public class Command : IRequest } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewModules], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var version = await _db.ModuleVersions.FirstOrDefaultAsync(v => v.Id == request.VersionId); + var version = await dbContext.ModuleVersions.FirstOrDefaultAsync(v => v.Id == request.VersionId); if (version == null) throw new EntityNotFoundException(); diff --git a/src/Caster.Api/Features/Modules/Requests/Delete.cs b/src/Caster.Api/Features/Modules/Requests/Delete.cs index 26adef9..60c0ddf 100644 --- a/src/Caster.Api/Features/Modules/Requests/Delete.cs +++ b/src/Caster.Api/Features/Modules/Requests/Delete.cs @@ -4,16 +4,13 @@ using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using MediatR; using Caster.Api.Data; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Modules { @@ -25,34 +22,20 @@ public class Command : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageModules], cancellationToken); - public Handler( - CasterContext db, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var entry = await _db.Modules.FirstOrDefaultAsync(m => m.Id == request.Id); + var module = await dbContext.Modules.FindAsync([request.Id], cancellationToken); - if (entry == null) + if (module == null) throw new EntityNotFoundException(); - _db.Modules.Remove(entry); - await _db.SaveChangesAsync(cancellationToken); + dbContext.Modules.Remove(module); + await dbContext.SaveChangesAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Modules/Requests/Get.cs b/src/Caster.Api/Features/Modules/Requests/Get.cs index 78c1d57..5d76d72 100644 --- a/src/Caster.Api/Features/Modules/Requests/Get.cs +++ b/src/Caster.Api/Features/Modules/Requests/Get.cs @@ -10,17 +10,16 @@ using Microsoft.EntityFrameworkCore; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using System.Linq; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Modules { public class Get { - [DataContract(Name="GetModuleQuery")] + [DataContract(Name = "GetModuleQuery")] public class Query : IRequest { /// @@ -30,31 +29,23 @@ public class Query : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task Authorize(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); + if (authorizationService.GetAuthorizedProjectIds().Any()) + { + return true; + } + else + { + return await authorizationService.Authorize([SystemPermission.ViewModules], cancellationToken); + } } - public async Task Handle(Query request, CancellationToken cancellationToken) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var module = await _db.Modules + var module = await dbContext.Modules .Include(m => m.Versions) .SingleOrDefaultAsync(x => x.Id == request.Id, cancellationToken); @@ -63,7 +54,7 @@ public async Task Handle(Query request, CancellationToken cancellationTo module.Versions = module.Versions.OrderByDescending(x => x.DateCreated).ToList(); - return _mapper.Map(module); + return mapper.Map(module); } } } diff --git a/src/Caster.Api/Features/Modules/Requests/GetAll.cs b/src/Caster.Api/Features/Modules/Requests/GetAll.cs index 851a521..2322f08 100644 --- a/src/Caster.Api/Features/Modules/Requests/GetAll.cs +++ b/src/Caster.Api/Features/Modules/Requests/GetAll.cs @@ -9,14 +9,12 @@ using Microsoft.EntityFrameworkCore; using AutoMapper.QueryableExtensions; using System.Runtime.Serialization; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Services; -using Caster.Api.Infrastructure.Identity; using System; using System.Linq; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Modules { @@ -50,46 +48,39 @@ public class Query : IRequest public Guid? DesignId { get; set; } } - public class Handler : IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + IGitlabRepositoryService gitlabRepositoryService) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly IGitlabRepositoryService _gitlabRepositoryService; - - public Handler( - CasterContext db, - IMapper mapper, - IGitlabRepositoryService gitlabRepositoryService, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task Authorize(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _gitlabRepositoryService = gitlabRepositoryService; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); + if (authorizationService.GetAuthorizedProjectIds().Any()) + { + return true; + } + else + { + return await authorizationService.Authorize([SystemPermission.ViewModules], cancellationToken); + } } - public async Task Handle(Query request, CancellationToken cancellationToken) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - // TODO: add handling for other repositories? // get all modules from the repository and update the database try { - await _gitlabRepositoryService.GetModulesAsync(request.ForceUpdate, cancellationToken); + await gitlabRepositoryService.GetModulesAsync(request.ForceUpdate, cancellationToken); } catch (Exception) { } - IQueryable query = _db.Modules; + IQueryable query = dbContext.Modules; if (request.DesignId.HasValue) { - var designQuery = _db.DesignModules + var designQuery = dbContext.DesignModules .Include(x => x.Module) .Where(x => x.DesignId == request.DesignId); @@ -108,7 +99,7 @@ public async Task Handle(Query request, CancellationToken cancellation } var modules = await query - .ProjectTo(_mapper.ConfigurationProvider, (request.IncludeVersionCount ? (dest => dest.VersionsCount) : null)) + .ProjectTo(mapper.ConfigurationProvider, request.IncludeVersionCount ? (dest => dest.VersionsCount) : null) .ToArrayAsync(cancellationToken); return modules; diff --git a/src/Caster.Api/Features/Modules/Requests/GetVersions.cs b/src/Caster.Api/Features/Modules/Requests/GetVersions.cs index 8783908..e9a0f0a 100644 --- a/src/Caster.Api/Features/Modules/Requests/GetVersions.cs +++ b/src/Caster.Api/Features/Modules/Requests/GetVersions.cs @@ -11,18 +11,15 @@ using Microsoft.EntityFrameworkCore; using AutoMapper.QueryableExtensions; using System.Runtime.Serialization; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Modules { public class GetVersions { - [DataContract(Name="GetVersionsQuery")] + [DataContract(Name = "GetVersionsQuery")] public class Query : IRequest { /// @@ -32,34 +29,27 @@ public class Query : IRequest public Guid ModuleId { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task Authorize(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); + if (authorizationService.GetAuthorizedProjectIds().Any()) + { + return true; + } + else + { + return await authorizationService.Authorize([SystemPermission.ViewModules], cancellationToken); + } } - public async Task Handle(Query request, CancellationToken cancellationToken) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - return await _db.ModuleVersions.Where(v => v.ModuleId == request.ModuleId) - .ProjectTo(_mapper.ConfigurationProvider) + return await dbContext.ModuleVersions + .Where(v => v.ModuleId == request.ModuleId) + .ProjectTo(mapper.ConfigurationProvider) .OrderByDescending(v => v.DateCreated) - .ToArrayAsync(); + .ToArrayAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Permissions/Permission.cs b/src/Caster.Api/Features/Permissions/Permission.cs deleted file mode 100644 index 2d12c11..0000000 --- a/src/Caster.Api/Features/Permissions/Permission.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System; - -namespace Caster.Api.Features.Permissions -{ - public class Permission - { - public Guid Id { get; set; } - - public string Key { get; set; } - - public string Value { get; set; } - - public string Description { get; set; } - - public bool ReadOnly { get; set; } - } -} - diff --git a/src/Caster.Api/Features/Permissions/PermissionsController.cs b/src/Caster.Api/Features/Permissions/PermissionsController.cs deleted file mode 100644 index 40c55da..0000000 --- a/src/Caster.Api/Features/Permissions/PermissionsController.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Swashbuckle.AspNetCore.Annotations; - -namespace Caster.Api.Features.Permissions -{ - [Route("api/permissions")] - [ApiController] - [Authorize] - public class PermissionsController : ControllerBase - { - private readonly IMediator _mediator; - - public PermissionsController(IMediator mediator) - { - _mediator = mediator; - } - - /// - /// Get a single permission. - /// - /// ID of an permission. - /// - [HttpGet("{id}")] - [ProducesResponseType(typeof(Permission), (int)HttpStatusCode.OK)] - [SwaggerOperation(OperationId = "GetPermission")] - public async Task Get([FromRoute] Guid id) - { - var result = await _mediator.Send(new Get.Query { Id = id }); - return Ok(result); - } - - /// - /// Get all permissions. - /// - /// - [HttpGet()] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [SwaggerOperation(OperationId = "GetAllPermissions")] - public async Task GetAll() - { - var result = await _mediator.Send(new GetAll.Query()); - return Ok(result); - } - - /// - /// Get permissions for the current user. - /// - /// - [HttpGet("mine")] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [SwaggerOperation(OperationId = "GetMyPermissions")] - public async Task GetMine() - { - var result = await _mediator.Send(new GetMine.Query()); - return Ok(result); - } - - /// - /// Get permissions for a user. - /// - /// ID of a user. - /// - [HttpGet("/users/{userId}/permissions")] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [SwaggerOperation(OperationId = "GetPermissionsByUser")] - public async Task GetByUser([FromRoute] Guid userId) - { - var result = await _mediator.Send(new GetPermissionsByUser.Query() { UserId = userId }); - return Ok(result); - } - - } -} - diff --git a/src/Caster.Api/Features/Permissions/Requests/Get.cs b/src/Caster.Api/Features/Permissions/Requests/Get.cs deleted file mode 100644 index 7a77790..0000000 --- a/src/Caster.Api/Features/Permissions/Requests/Get.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System; -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using AutoMapper; -using Microsoft.EntityFrameworkCore; -using AutoMapper.QueryableExtensions; -using System.Runtime.Serialization; -using Caster.Api.Data; -using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; -using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; - -namespace Caster.Api.Features.Permissions -{ - public class Get - { - [DataContract(Name="GetPermissionQuery")] - public class Query : IRequest - { - /// - /// The Id of the Permission to retrieve - /// - [DataMember] - public Guid Id { get; set; } - } - - public class Handler : IRequestHandler - { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var permission = await _db.Permissions - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(e => e.Id == request.Id); - - if (permission == null) - throw new EntityNotFoundException(); - - return permission; - } - } - } -} - diff --git a/src/Caster.Api/Features/Permissions/Requests/GetAll.cs b/src/Caster.Api/Features/Permissions/Requests/GetAll.cs deleted file mode 100644 index d1daa2d..0000000 --- a/src/Caster.Api/Features/Permissions/Requests/GetAll.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using AutoMapper; -using Microsoft.EntityFrameworkCore; -using AutoMapper.QueryableExtensions; -using System.Runtime.Serialization; -using Caster.Api.Data; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; -using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; - -namespace Caster.Api.Features.Permissions -{ - public class GetAll - { - [DataContract(Name="GetPermissionsQuery")] - public class Query : IRequest - { - } - - public class Handler : IRequestHandler - { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - return await _db.Permissions - .ProjectTo(_mapper.ConfigurationProvider) - .ToArrayAsync(); - } - } - } -} - diff --git a/src/Caster.Api/Features/Permissions/Requests/GetByUser.cs b/src/Caster.Api/Features/Permissions/Requests/GetByUser.cs deleted file mode 100644 index bada978..0000000 --- a/src/Caster.Api/Features/Permissions/Requests/GetByUser.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using AutoMapper; -using Microsoft.EntityFrameworkCore; -using AutoMapper.QueryableExtensions; -using System.Runtime.Serialization; -using Caster.Api.Data; -using Caster.Api.Features.Permissions; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; -using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; - -namespace Caster.Api.Features.Permissions -{ - public class GetPermissionsByUser - { - [DataContract(Name="GetPermissionsByUserQuery")] - public class Query : IRequest - { - /// - /// The Id of the User to query by - /// - [DataMember] - public Guid UserId { get; set; } - } - - public class Handler : IRequestHandler - { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var permissions = await _db.Permissions.Where(p => p.UserPermissions.Any(up => up.UserId == request.UserId)) - .ProjectTo(_mapper.ConfigurationProvider) - .ToArrayAsync(); - - return permissions; - } - } - } -} - diff --git a/src/Caster.Api/Features/Permissions/Requests/GetMine.cs b/src/Caster.Api/Features/Permissions/Requests/GetMine.cs deleted file mode 100644 index b8b0eb4..0000000 --- a/src/Caster.Api/Features/Permissions/Requests/GetMine.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using AutoMapper; -using Microsoft.EntityFrameworkCore; -using AutoMapper.QueryableExtensions; -using System.Runtime.Serialization; -using Caster.Api.Data; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; -using Caster.Api.Infrastructure.Extensions; -using Caster.Api.Infrastructure.Identity; - -namespace Caster.Api.Features.Permissions -{ - public class GetMine - { - [DataContract(Name="GetMyPermissionsQuery")] - public class Query : IRequest - { - } - - public class Handler : IRequestHandler - { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - return await _db.UserPermissions - .Where(w => w.UserId == _user.GetId()) - .Select(x => x.Permission) - .ProjectTo(_mapper.ConfigurationProvider) - .ToArrayAsync(); - } - } - } -} - diff --git a/src/Caster.Api/Features/Plans/Requests/Get.cs b/src/Caster.Api/Features/Plans/Requests/Get.cs index e199063..b08a416 100644 --- a/src/Caster.Api/Features/Plans/Requests/Get.cs +++ b/src/Caster.Api/Features/Plans/Requests/Get.cs @@ -11,17 +11,15 @@ using AutoMapper.QueryableExtensions; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Plans { public class Get { - [DataContract(Name="GetPlanQuery")] + [DataContract(Name = "GetPlanQuery")] public class Query : IRequest { /// @@ -31,32 +29,15 @@ public class Query : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var plan = await _db.Plans - .ProjectTo(_mapper.ConfigurationProvider) + var plan = await dbContext.Plans + .ProjectTo(mapper.ConfigurationProvider) .SingleOrDefaultAsync(x => x.Id == request.Id, cancellationToken); if (plan == null) diff --git a/src/Caster.Api/Features/Plans/Requests/GetByRun.cs b/src/Caster.Api/Features/Plans/Requests/GetByRun.cs index 204cb86..0cfd5bb 100644 --- a/src/Caster.Api/Features/Plans/Requests/GetByRun.cs +++ b/src/Caster.Api/Features/Plans/Requests/GetByRun.cs @@ -18,12 +18,16 @@ using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using FluentValidation; +using Caster.Api.Features.Shared.Services; +using Caster.Api.Infrastructure.Extensions; namespace Caster.Api.Features.Plans { public class GetByRun { - [DataContract(Name="GetPlanByRunQuery")] + [DataContract(Name = "GetPlanByRunQuery")] public class Query : IRequest { /// @@ -34,34 +38,23 @@ public class Query : IRequest public Guid RunId { get; set; } } - public class Handler : IRequestHandler + public class RequestValidator : AbstractValidator { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public RequestValidator(IValidationService validationService) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); + RuleFor(x => x.RunId).RunExists(validationService); } + } - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - await ValidateRun(request.RunId); + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.RunId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - var plan = await _db.Plans - .ProjectTo(_mapper.ConfigurationProvider) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { + var plan = await dbContext.Plans + .ProjectTo(mapper.ConfigurationProvider) .SingleOrDefaultAsync(x => x.RunId == request.RunId, cancellationToken); if (plan == null) @@ -69,14 +62,6 @@ public async Task Handle(Query request, CancellationToken cancellationToke return plan; } - - private async Task ValidateRun(Guid runId) - { - var run = await _db.Runs.FindAsync(runId); - - if (run == null) - throw new EntityNotFoundException(); - } } } } diff --git a/src/Caster.Api/Features/Permissions/MappingProfile.cs b/src/Caster.Api/Features/ProjectPermissions/MappingProfile.cs similarity index 68% rename from src/Caster.Api/Features/Permissions/MappingProfile.cs rename to src/Caster.Api/Features/ProjectPermissions/MappingProfile.cs index fa4fbbc..0d0d9ec 100644 --- a/src/Caster.Api/Features/Permissions/MappingProfile.cs +++ b/src/Caster.Api/Features/ProjectPermissions/MappingProfile.cs @@ -1,15 +1,14 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. -namespace Caster.Api.Features.Permissions +namespace Caster.Api.Features.ProjectPermissions { - using AutoMapper; + using AutoMapper; public class MappingProfile : Profile { public MappingProfile() { - CreateMap(); } } } diff --git a/src/Caster.Api/Features/ProjectPermissions/ProjectPermissionsController.cs b/src/Caster.Api/Features/ProjectPermissions/ProjectPermissionsController.cs new file mode 100644 index 0000000..210d773 --- /dev/null +++ b/src/Caster.Api/Features/ProjectPermissions/ProjectPermissionsController.cs @@ -0,0 +1,39 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Caster.Api.Infrastructure.Authorization; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Caster.Api.Features.ProjectPermissions; + +[Route("api/permissions/project")] +[ApiController] +[Authorize] +public class ProjectPermissionsController : ControllerBase +{ + private readonly IMediator _mediator; + + public ProjectPermissionsController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// Get all SystemPermissions for the calling User. + /// + /// + [HttpGet("mine")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "GetMyProjectPermissions")] + public async Task GetAll([FromQuery] GetMine.Query query) + { + var result = await _mediator.Send(query); + return Ok(result); + } +} \ No newline at end of file diff --git a/src/Caster.Api/Features/ProjectPermissions/Requests/GetMine.cs b/src/Caster.Api/Features/ProjectPermissions/Requests/GetMine.cs new file mode 100644 index 0000000..d9686df --- /dev/null +++ b/src/Caster.Api/Features/ProjectPermissions/Requests/GetMine.cs @@ -0,0 +1,35 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using System.Runtime.Serialization; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Features.Shared; +using System.Linq; +using System; + +namespace Caster.Api.Features.ProjectPermissions +{ + public class GetMine + { + [DataContract(Name = "GetMyProjectPermissionsQuery")] + public record Query : IRequest + { + [DataMember] + public Guid? ProjectId { get; set; } + } + + public class Handler(ICasterAuthorizationService authorizationService) : BaseHandler + { + public override Task Authorize(Query request, CancellationToken cancellationToken) => Task.FromResult(true); + + public override Task HandleRequest(Query request, CancellationToken cancellationToken) + { + return Task.FromResult(authorizationService.GetProjectPermissions(request.ProjectId).ToArray()); + } + } + } +} + diff --git a/src/Caster.Api/Features/UserPermissions/MappingProfile.cs b/src/Caster.Api/Features/ProjectRoles/MappingProfile.cs similarity index 50% rename from src/Caster.Api/Features/UserPermissions/MappingProfile.cs rename to src/Caster.Api/Features/ProjectRoles/MappingProfile.cs index b80fe7d..8f7faef 100644 --- a/src/Caster.Api/Features/UserPermissions/MappingProfile.cs +++ b/src/Caster.Api/Features/ProjectRoles/MappingProfile.cs @@ -1,17 +1,16 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. -namespace Caster.Api.Features.UserPermissions +namespace Caster.Api.Features.ProjectRoles { - using AutoMapper; + using AutoMapper; + using System.Linq; public class MappingProfile : Profile { public MappingProfile() { - CreateMap(); - CreateMap(); - CreateMap(); + CreateMap(); } } } diff --git a/src/Caster.Api/Features/ProjectRoles/ProjectRole.cs b/src/Caster.Api/Features/ProjectRoles/ProjectRole.cs new file mode 100644 index 0000000..17c3c3a --- /dev/null +++ b/src/Caster.Api/Features/ProjectRoles/ProjectRole.cs @@ -0,0 +1,19 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; + +namespace Caster.Api.Features.ProjectRoles +{ + public class ProjectRole + { + + public Guid Id { get; set; } + + public string Name { get; set; } + public bool AllPermissions { get; set; } + + public Domain.Models.ProjectPermission[] Permissions { get; set; } + } +} + diff --git a/src/Caster.Api/Features/ProjectRoles/ProjectRolesController.cs b/src/Caster.Api/Features/ProjectRoles/ProjectRolesController.cs new file mode 100644 index 0000000..3e02c03 --- /dev/null +++ b/src/Caster.Api/Features/ProjectRoles/ProjectRolesController.cs @@ -0,0 +1,53 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Caster.Api.Features.ProjectRoles; + +[Route("api/project-roles")] +[ApiController] +[Authorize] +public class ProjectRolesController : ControllerBase +{ + private readonly IMediator _mediator; + + public ProjectRolesController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// Get a single ProjectRole. + /// + /// ID of an ProjectRole. + /// + [HttpGet("{id}")] + [ProducesResponseType(typeof(ProjectRole), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "GetProjectRole")] + public async Task Get([FromRoute] Guid id) + { + var result = await _mediator.Send(new Get.Query { Id = id }); + return Ok(result); + } + + /// + /// Get all ProjectRoles. + /// + /// + [HttpGet()] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "GetAllProjectRoles")] + public async Task GetAll() + { + var result = await _mediator.Send(new GetAll.Query()); + return Ok(result); + } +} \ No newline at end of file diff --git a/src/Caster.Api/Features/ProjectRoles/Requests/Get.cs b/src/Caster.Api/Features/ProjectRoles/Requests/Get.cs new file mode 100644 index 0000000..d86e071 --- /dev/null +++ b/src/Caster.Api/Features/ProjectRoles/Requests/Get.cs @@ -0,0 +1,55 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using AutoMapper.QueryableExtensions; +using System.Runtime.Serialization; +using Caster.Api.Data; +using System.Security.Claims; +using System.Security.Principal; +using Microsoft.AspNetCore.Authorization; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Infrastructure.Exceptions; +using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Features.ProjectRoles +{ + public class Get + { + [DataContract(Name = "GetProjectRoleQuery")] + public class Query : IRequest + { + /// + /// The Id of the ProjectRole to retrieve + /// + [DataMember] + public Guid Id { get; set; } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewRoles], cancellationToken); + + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { + var projectRole = await dbContext.ProjectRoles + .ProjectTo(mapper.ConfigurationProvider, dest => dest.Permissions) + .SingleOrDefaultAsync(e => e.Id == request.Id, cancellationToken); + + if (projectRole == null) + throw new EntityNotFoundException(); + + return projectRole; + } + } + } +} + diff --git a/src/Caster.Api/Features/ProjectRoles/Requests/GetAll.cs b/src/Caster.Api/Features/ProjectRoles/Requests/GetAll.cs new file mode 100644 index 0000000..b4f6ed8 --- /dev/null +++ b/src/Caster.Api/Features/ProjectRoles/Requests/GetAll.cs @@ -0,0 +1,49 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using AutoMapper.QueryableExtensions; +using System.Runtime.Serialization; +using Caster.Api.Data; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; +using System.Linq; + +namespace Caster.Api.Features.ProjectRoles +{ + public class GetAll + { + [DataContract(Name = "GetProjectRolesQuery")] + public class Query : IRequest + { + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) + { + if (await authorizationService.Authorize([SystemPermission.ViewRoles, SystemPermission.ViewProjects], cancellationToken)) + { + return true; + } + + return authorizationService. + GetProjectPermissions() + .Any(x => x.Permissions.Contains(ProjectPermission.ManageProject)); + } + + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { + return await dbContext.ProjectRoles + .ProjectTo(mapper.ConfigurationProvider) + .ToArrayAsync(cancellationToken); + } + } + } +} + diff --git a/src/Caster.Api/Features/Projects/EventHandlers/AuthCacheEventHandler.cs b/src/Caster.Api/Features/Projects/EventHandlers/AuthCacheEventHandler.cs new file mode 100644 index 0000000..e0783b1 --- /dev/null +++ b/src/Caster.Api/Features/Projects/EventHandlers/AuthCacheEventHandler.cs @@ -0,0 +1,64 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Caster.Api.Data; +using Caster.Api.Domain.Events; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; + +namespace Caster.Api.Features.Projects.EventHandlers; + +public class ProjectMembershipBaseAuthCacheHandler(IMemoryCache cache, CasterContext dbContext) +{ + protected async Task UpdateCache(Domain.Models.ProjectMembership membership) + { + if (membership.UserId.HasValue) + { + cache.Remove(membership.UserId); + } + + if (membership.GroupId.HasValue) + { + var userIds = await dbContext.GroupMemberships + .Where(x => x.GroupId == membership.GroupId.Value) + .Select(x => x.UserId) + .ToListAsync(); + + foreach (var userId in userIds) + { + cache.Remove(userId); + } + } + } +} + +public class ProjectMembershipCreatedAuthCacheHandler(IMemoryCache memoryCache, CasterContext db) : ProjectMembershipBaseAuthCacheHandler(memoryCache, db), + INotificationHandler> +{ + public async Task Handle(EntityCreated notification, CancellationToken cancellationToken) + { + await UpdateCache(notification.Entity); + } +} + +public class ProjectMembershipUpdatedAuthCacheHandler(IMemoryCache memoryCache, CasterContext db) : ProjectMembershipBaseAuthCacheHandler(memoryCache, db), + INotificationHandler> +{ + public async Task Handle(EntityUpdated notification, CancellationToken cancellationToken) + { + await UpdateCache(notification.Entity); + } +} + +public class ProjectMembershipDeletedAuthCacheHandler(IMemoryCache memoryCache, CasterContext db) : ProjectMembershipBaseAuthCacheHandler(memoryCache, db), + INotificationHandler> +{ + public async Task Handle(EntityDeleted notification, CancellationToken cancellationToken) + { + await UpdateCache(notification.Entity); + } +} \ No newline at end of file diff --git a/src/Caster.Api/Features/Projects/EventHandlers/SignalREventHandler.cs b/src/Caster.Api/Features/Projects/EventHandlers/SignalREventHandler.cs new file mode 100644 index 0000000..862df16 --- /dev/null +++ b/src/Caster.Api/Features/Projects/EventHandlers/SignalREventHandler.cs @@ -0,0 +1,58 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Caster.Api.Data; +using Caster.Api.Domain.Events; +using Caster.Api.Hubs; +using MediatR; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; + +namespace Caster.Api.Features.Projects.EventHandlers; + +public class ProjectMembershipCreatedSignalRHandler(CasterContext _db, IMapper _mapper, IHubContext _projectHub) : + ProjectMembershipBaseSignalRHandler(_db, _mapper, _projectHub), + INotificationHandler> +{ + public async Task Handle(EntityCreated notification, CancellationToken cancellationToken) + { + await base.Handle(notification.Entity, ProjectHubMethods.ProjectMembershipCreated, null, cancellationToken); + } +} + +public class ProjectMembershipUpdatedSignalRHandler(CasterContext _db, IMapper _mapper, IHubContext _projectHub) : + ProjectMembershipBaseSignalRHandler(_db, _mapper, _projectHub), + INotificationHandler> +{ + public async Task Handle(EntityUpdated notification, CancellationToken cancellationToken) + { + await base.Handle(notification.Entity, ProjectHubMethods.ProjectMembershipUpdated, notification.ModifiedProperties, cancellationToken); + } +} + +public class ProjectMembershipDeletedSignalRHandler(IHubContext projectHub) : + INotificationHandler> +{ + public async Task Handle(EntityDeleted notification, CancellationToken cancellationToken) + { + await projectHub.Clients.Group(ProjectHubMethods.GetProjectAdminGroup(notification.Entity.ProjectId)).SendAsync(ProjectHubMethods.ProjectMembershipDeleted, notification.Entity.Id); + } +} + +public class ProjectMembershipBaseSignalRHandler(CasterContext db, IMapper mapper, IHubContext projectHub) +{ + protected async Task Handle(Domain.Models.ProjectMembership entity, string method, string[] modifiedProperties, CancellationToken cancellationToken) + { + var projectMembership = await db.ProjectMemberships + .Where(x => x.Id == entity.Id) + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(); + + await projectHub.Clients.Group(ProjectHubMethods.GetProjectAdminGroup(entity.ProjectId)).SendAsync(method, projectMembership, modifiedProperties, cancellationToken); + } +} diff --git a/src/Caster.Api/Features/Projects/MappingProfile.cs b/src/Caster.Api/Features/Projects/MappingProfile.cs index 7aa5f0b..3f48c2f 100644 --- a/src/Caster.Api/Features/Projects/MappingProfile.cs +++ b/src/Caster.Api/Features/Projects/MappingProfile.cs @@ -16,6 +16,8 @@ public MappingProfile() CreateMap(); CreateMap() .ForMember(dest => dest.LockedFiles, opt => opt.MapFrom((src) => src.LockedFiles.Select(x => x.Path))); + + CreateMap(); } } } diff --git a/src/Caster.Api/Features/Projects/Project.cs b/src/Caster.Api/Features/Projects/Project.cs index d8e0ca3..c7afd93 100644 --- a/src/Caster.Api/Features/Projects/Project.cs +++ b/src/Caster.Api/Features/Projects/Project.cs @@ -11,10 +11,12 @@ public class Project /// ID of the project. /// public Guid Id { get; set; } + /// /// Name of the project. /// public string Name { get; set; } + /// /// The vlan partition this project is a part of. /// diff --git a/src/Caster.Api/Features/Projects/ProjectMembership.cs b/src/Caster.Api/Features/Projects/ProjectMembership.cs new file mode 100644 index 0000000..fecdabb --- /dev/null +++ b/src/Caster.Api/Features/Projects/ProjectMembership.cs @@ -0,0 +1,32 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; + +namespace Caster.Api.Features.Projects +{ + public class ProjectMembership + { + public Guid Id { get; set; } + + /// + /// ID of the project. + /// + public Guid ProjectId { get; set; } + + /// + /// Id of the User. + /// + public Guid? UserId { get; set; } + + /// + /// Id of the Group. + /// + public Guid? GroupId { get; set; } + + /// + /// Id of the Role this User has for this Project + /// + public Guid? RoleId { get; set; } + } +} diff --git a/src/Caster.Api/Features/Projects/ProjectsController.cs b/src/Caster.Api/Features/Projects/ProjectsController.cs index 975efb2..b4872c8 100644 --- a/src/Caster.Api/Features/Projects/ProjectsController.cs +++ b/src/Caster.Api/Features/Projects/ProjectsController.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Generic; +using System.Data; using System.Net; using System.Threading.Tasks; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis; using Swashbuckle.AspNetCore.Annotations; namespace Caster.Api.Features.Projects @@ -76,9 +78,9 @@ public async Task Import([FromRoute] Guid id, [FromQuery] Import. [HttpGet()] [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] [SwaggerOperation(OperationId = "GetAllProjects")] - public async Task GetAll() + public async Task GetAll([FromQuery] GetAll.Query query) { - var result = await _mediator.Send(new GetAll.Query()); + var result = await _mediator.Send(query); return Ok(result); } @@ -113,7 +115,7 @@ public async Task Edit([FromRoute] Guid id, Edit.Command command) } /// - /// Delete an project. + /// Delete a project. /// /// ID of an project. /// @@ -125,5 +127,73 @@ public async Task Delete([FromRoute] Guid id) await _mediator.Send(new Delete.Command { Id = id }); return NoContent(); } + + /// + /// Get a single Project Membership. + /// + /// + [HttpGet("memberships/{id}")] + [ProducesResponseType(typeof(ProjectMembership), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "GetProjectMembership")] + public async Task GetMembership([FromRoute] Guid id) + { + var result = await _mediator.Send(new GetMembership.Query { Id = id }); + return Ok(result); + } + + /// + /// Get all Project Memberships of a Project. + /// + /// + [HttpGet("{projectId}/memberships")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "GetProjectMemberships")] + public async Task GetMemberships([FromRoute] Guid projectId) + { + var result = await _mediator.Send(new GetMemberships.Query() { ProjectId = projectId }); + return Ok(result); + } + + /// + /// Create a new Project Membership. + /// + /// + /// + /// + [HttpPost("{projectId}/memberships")] + [ProducesResponseType(typeof(ProjectMembership), (int)HttpStatusCode.Created)] + [SwaggerOperation(OperationId = "CreateProjectMembership")] + public async Task CreateMembership([FromRoute] Guid projectId, CreateMembership.Command command) + { + command.ProjectId = projectId; + var result = await _mediator.Send(command); + return CreatedAtAction(nameof(Get), new { id = result.Id }, result); + } + + /// + /// Edit a Project Membership. + /// + /// + [HttpPut("memberships")] + [ProducesResponseType(typeof(ProjectMembership), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "EditProjectMembership")] + public async Task EditMembership(EditMembership.Command command) + { + var result = await _mediator.Send(command); + return Ok(result); + } + + /// + /// Delete a Project Membership. + /// + /// + [HttpDelete("memberships/{id}")] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + [SwaggerOperation(OperationId = "DeleteProjectMembership")] + public async Task DeleteMembership([FromRoute] Guid id) + { + await _mediator.Send(new DeleteMembership.Command { Id = id }); + return NoContent(); + } } } diff --git a/src/Caster.Api/Features/Projects/Requests/Create.cs b/src/Caster.Api/Features/Projects/Requests/Create.cs index 8596968..79255fa 100644 --- a/src/Caster.Api/Features/Projects/Requests/Create.cs +++ b/src/Caster.Api/Features/Projects/Requests/Create.cs @@ -13,12 +13,16 @@ using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Infrastructure.Identity; +using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Domain.Models; +using Microsoft.Extensions.DependencyInjection; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Projects { public class Create { - [DataContract(Name="CreateProjectCommand")] + [DataContract(Name = "CreateProjectCommand")] public class Command : IRequest { /// @@ -28,34 +32,25 @@ public class Command : IRequest public string Name { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext, IIdentityResolver identityResolver) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.CreateProjects], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } + var project = mapper.Map(request); + dbContext.Projects.Add(project); - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); + // Add the creator as a member with the appropriate role + var projectMembership = new Domain.Models.ProjectMembership(); + projectMembership.UserId = identityResolver.GetClaimsPrincipal().GetId(); + projectMembership.Project = project; + projectMembership.RoleId = ProjectRoleDefaults.ProjectCreatorRoleId; + dbContext.ProjectMemberships.Add(projectMembership); - var project = _mapper.Map(request); - await _db.Projects.AddAsync(project); - await _db.SaveChangesAsync(); - return _mapper.Map(project); + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(project); } } } diff --git a/src/Caster.Api/Features/Projects/Requests/CreateMembership.cs b/src/Caster.Api/Features/Projects/Requests/CreateMembership.cs new file mode 100644 index 0000000..9ca35a0 --- /dev/null +++ b/src/Caster.Api/Features/Projects/Requests/CreateMembership.cs @@ -0,0 +1,78 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Caster.Api.Data; +using AutoMapper; +using System.Runtime.Serialization; +using Caster.Api.Infrastructure.Exceptions; +using Microsoft.EntityFrameworkCore; +using FluentValidation; +using Caster.Api.Features.Shared.Services; +using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Domain.Models; +using System.Text.Json.Serialization; +using Caster.Api.Features.Shared; +using Caster.Api.Infrastructure.Authorization; + +namespace Caster.Api.Features.Projects +{ + public class CreateMembership + { + [DataContract(Name = "CreateProjectMembershipCommand")] + public record Command : IRequest + { + /// + /// The Id of the Project to add to. + /// + [JsonIgnore] + public Guid ProjectId { get; set; } + + /// + /// The Id of the User to add. + /// + [DataMember] + public Guid? UserId { get; set; } + + /// + /// The Id of the Group to add. + /// + [DataMember] + public Guid? GroupId { get; set; } + } + + public class Validator : AbstractValidator + { + public Validator(IValidationService validationService) + { + RuleFor(x => x.ProjectId).ProjectExists(validationService); + RuleFor(x => x.UserId.Value).UserExists(validationService).When(x => x.UserId.HasValue); + RuleFor(x => x.GroupId.Value).GroupExists(validationService).When(x => x.GroupId.HasValue); + } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.ProjectId, [SystemPermission.ManageProjects], [ProjectPermission.ManageProject], cancellationToken); + + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) + { + var projectMembershipExists = await dbContext.ProjectMemberships + .AnyAsync(x => x.ProjectId == request.ProjectId && x.UserId == request.UserId && x.GroupId == request.GroupId, cancellationToken); + + if (projectMembershipExists) + throw new ConflictException("ProjectMembership already exists"); + + var projectMembership = new Domain.Models.ProjectMembership(request.ProjectId, request.UserId, request.GroupId); + dbContext.ProjectMemberships.Add(projectMembership); + await dbContext.SaveChangesAsync(cancellationToken); + + return mapper.Map(projectMembership); + } + } + } +} diff --git a/src/Caster.Api/Features/Projects/Requests/Delete.cs b/src/Caster.Api/Features/Projects/Requests/Delete.cs index 5bbc06c..f7eaf90 100644 --- a/src/Caster.Api/Features/Projects/Requests/Delete.cs +++ b/src/Caster.Api/Features/Projects/Requests/Delete.cs @@ -17,6 +17,8 @@ using AutoMapper.QueryableExtensions; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Projects { @@ -28,37 +30,20 @@ public class Command : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var entry = _db.Projects.FirstOrDefault(e => e.Id == request.Id); + var project = await dbContext.Projects.FindAsync([request.Id], cancellationToken); - if (entry == null) + if (project == null) throw new EntityNotFoundException(); - _db.Projects.Remove(entry); - await _db.SaveChangesAsync(cancellationToken); + dbContext.Projects.Remove(project); + await dbContext.SaveChangesAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Projects/Requests/DeleteMembership.cs b/src/Caster.Api/Features/Projects/Requests/DeleteMembership.cs new file mode 100644 index 0000000..79fb0b2 --- /dev/null +++ b/src/Caster.Api/Features/Projects/Requests/DeleteMembership.cs @@ -0,0 +1,47 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Caster.Api.Data; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Caster.Api.Infrastructure.Exceptions; +using Caster.Api.Features.Shared; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Features.Projects +{ + public class DeleteMembership + { + [DataContract(Name = "DeleteProjectMembershipCommand")] + public record Command : IRequest + { + /// + /// The Id of the ProjectMembership to delete. + /// + [JsonIgnore] + public Guid Id { get; set; } + } + + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ManageProjects], [ProjectPermission.ManageProject], cancellationToken); + + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) + { + var projectMembership = await dbContext.ProjectMemberships.FindAsync([request.Id], cancellationToken); + + if (projectMembership == null) + throw new EntityNotFoundException(); + + dbContext.ProjectMemberships.Remove(projectMembership); + await dbContext.SaveChangesAsync(cancellationToken); + } + } + } +} diff --git a/src/Caster.Api/Features/Projects/Requests/Edit.cs b/src/Caster.Api/Features/Projects/Requests/Edit.cs index 0c4f87f..a24f050 100644 --- a/src/Caster.Api/Features/Projects/Requests/Edit.cs +++ b/src/Caster.Api/Features/Projects/Requests/Edit.cs @@ -9,19 +9,15 @@ using AutoMapper; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; -using Microsoft.EntityFrameworkCore; -using AutoMapper.QueryableExtensions; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Projects { public class Edit { - [DataContract(Name="EditProjectCommand")] + [DataContract(Name = "EditProjectCommand")] public class Command : IRequest { public Guid Id { get; set; } @@ -33,38 +29,21 @@ public class Command : IRequest public string Name { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var project = await _db.Projects.FindAsync(request.Id); + var project = await dbContext.Projects.FindAsync([request.Id], cancellationToken); if (project == null) throw new EntityNotFoundException(); - _mapper.Map(request, project); - await _db.SaveChangesAsync(); - return _mapper.Map(project); + mapper.Map(request, project); + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(project); } } } diff --git a/src/Caster.Api/Features/Projects/Requests/EditMembership.cs b/src/Caster.Api/Features/Projects/Requests/EditMembership.cs new file mode 100644 index 0000000..81cb96a --- /dev/null +++ b/src/Caster.Api/Features/Projects/Requests/EditMembership.cs @@ -0,0 +1,62 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Caster.Api.Data; +using AutoMapper; +using System.Runtime.Serialization; +using Caster.Api.Infrastructure.Exceptions; +using Caster.Api.Infrastructure.Authorization; +using FluentValidation; +using Caster.Api.Features.Shared.Services; +using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Features.Projects +{ + public class EditMembership + { + [DataContract(Name = "EditProjectMembershipCommand")] + public record Command : IRequest + { + /// + /// The Project Id of the Membership + /// + [DataMember] + public Guid Id { get; set; } + + [DataMember] + public Guid RoleId { get; set; } + } + + public class Validator : AbstractValidator + { + public Validator(IValidationService validationService) + { + RuleFor(x => x.RoleId).ProjectRoleExists(validationService); + } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ManageProjects], [ProjectPermission.ManageProject], cancellationToken); + + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) + { + var projectMembership = await dbContext.ProjectMemberships.FindAsync([request.Id], cancellationToken); + + if (projectMembership == null) + throw new EntityNotFoundException(); + + projectMembership.RoleId = request.RoleId; + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(projectMembership); + } + } + } +} diff --git a/src/Caster.Api/Features/Projects/Requests/Export.cs b/src/Caster.Api/Features/Projects/Requests/Export.cs index 84a21c2..ee1c292 100644 --- a/src/Caster.Api/Features/Projects/Requests/Export.cs +++ b/src/Caster.Api/Features/Projects/Requests/Export.cs @@ -5,24 +5,21 @@ using System.Threading; using System.Threading.Tasks; using MediatR; -using AutoMapper; using Caster.Api.Data; using Microsoft.EntityFrameworkCore; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using System.Text.Json.Serialization; using Caster.Api.Domain.Services; using Caster.Api.Domain.Models; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Projects { public class Export { - [DataContract(Name="ExportProjectQuery")] + [DataContract(Name = "ExportProjectQuery")] public class Query : IRequest { [JsonIgnore] @@ -35,34 +32,17 @@ public class Query : IRequest public bool IncludeIds { get; set; } } - public class Handler : IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + CasterContext dbContext, + IArchiveService archiveService) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly IArchiveService _archiveService; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - IArchiveService archiveService) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _archiveService = archiveService; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var project = await _db.Projects + var project = await dbContext.Projects .Include(e => e.Directories) .ThenInclude(d => d.Files) .Include(e => e.Directories) @@ -72,7 +52,7 @@ public async Task Handle(Query request, CancellationToken cancell if (project == null) throw new EntityNotFoundException(); - return await _archiveService.ArchiveProject(project, request.ArchiveType, request.IncludeIds); + return await archiveService.ArchiveProject(project, request.ArchiveType, request.IncludeIds); } } } diff --git a/src/Caster.Api/Features/Projects/Requests/Get.cs b/src/Caster.Api/Features/Projects/Requests/Get.cs index 8e06e85..6fe4ec0 100644 --- a/src/Caster.Api/Features/Projects/Requests/Get.cs +++ b/src/Caster.Api/Features/Projects/Requests/Get.cs @@ -11,17 +11,15 @@ using System.Runtime.Serialization; using Caster.Api.Data; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Projects { public class Get { - [DataContract(Name="GetProjectQuery")] + [DataContract(Name = "GetProjectQuery")] public class Query : IRequest { /// @@ -31,33 +29,16 @@ public class Query : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var project = await _db.Projects - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(e => e.Id == request.Id); + var project = await dbContext.Projects + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(e => e.Id == request.Id, cancellationToken); if (project == null) throw new EntityNotFoundException(); diff --git a/src/Caster.Api/Features/Projects/Requests/GetAll.cs b/src/Caster.Api/Features/Projects/Requests/GetAll.cs index ead507c..eecfd1c 100644 --- a/src/Caster.Api/Features/Projects/Requests/GetAll.cs +++ b/src/Caster.Api/Features/Projects/Requests/GetAll.cs @@ -2,56 +2,57 @@ // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. using System.Runtime.Serialization; -using System.Security.Claims; -using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore; using AutoMapper.QueryableExtensions; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; +using Azure.Core; +using System.Linq; namespace Caster.Api.Features.Projects { public class GetAll { - [DataContract(Name="GetProjectsQuery")] + [DataContract(Name = "GetProjectsQuery")] public class Query : IRequest { + [DataMember] + public bool OnlyMine { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task Authorize(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); + if (request.OnlyMine) + { + return true; + } + else + { + return await authorizationService.Authorize([SystemPermission.ViewProjects], cancellationToken); + } } - public async Task Handle(Query request, CancellationToken cancellationToken) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); + var query = dbContext.Projects.AsQueryable(); + + if (request.OnlyMine) + { + var projectIds = authorizationService.GetAuthorizedProjectIds(); + query = query.Where(x => projectIds.Contains(x.Id)); + } - return await _db.Projects - .ProjectTo(_mapper.ConfigurationProvider) - .ToArrayAsync(); + return await query + .ProjectTo(mapper.ConfigurationProvider) + .ToArrayAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Projects/Requests/GetMembership.cs b/src/Caster.Api/Features/Projects/Requests/GetMembership.cs new file mode 100644 index 0000000..1ef4e33 --- /dev/null +++ b/src/Caster.Api/Features/Projects/Requests/GetMembership.cs @@ -0,0 +1,54 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Caster.Api.Data; +using AutoMapper; +using System.Runtime.Serialization; +using Caster.Api.Infrastructure.Exceptions; +using Microsoft.EntityFrameworkCore; +using AutoMapper.QueryableExtensions; +using Caster.Api.Infrastructure.Authorization; +using FluentValidation; +using System.Linq; +using System.Text.Json.Serialization; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Features.Projects +{ + public class GetMembership + { + [DataContract(Name = "GetProjectMembershipQuery")] + public record Query : IRequest + { + /// + /// Id of the ProjectMembership + /// + [JsonIgnore] + public Guid Id { get; set; } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ManageProjects], [ProjectPermission.ManageProject], cancellationToken); + + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { + var projectMembership = await dbContext.ProjectMemberships + .Where(x => x.Id == request.Id) + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(cancellationToken); + + if (projectMembership == null) + throw new EntityNotFoundException(); + + return projectMembership; + } + } + } +} diff --git a/src/Caster.Api/Features/Projects/Requests/GetMemberships.cs b/src/Caster.Api/Features/Projects/Requests/GetMemberships.cs new file mode 100644 index 0000000..77dfc1d --- /dev/null +++ b/src/Caster.Api/Features/Projects/Requests/GetMemberships.cs @@ -0,0 +1,53 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using AutoMapper.QueryableExtensions; +using Caster.Api.Data; +using Caster.Api.Infrastructure.Authorization; +using System; +using FluentValidation; +using Caster.Api.Features.Shared.Services; +using Caster.Api.Infrastructure.Extensions; +using System.Linq; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Features.Projects +{ + public class GetMemberships + { + [DataContract(Name = "GetProjectMembershipsQuery")] + public record Query : IRequest + { + public Guid ProjectId { get; set; } + } + + public class Validator : AbstractValidator + { + public Validator(IValidationService validationService) + { + RuleFor(x => x.ProjectId).ProjectExists(validationService); + } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.ProjectId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); + + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { + return await dbContext.ProjectMemberships + .Where(x => x.ProjectId == request.ProjectId) + .ProjectTo(mapper.ConfigurationProvider) + .ToArrayAsync(cancellationToken); + } + } + } +} diff --git a/src/Caster.Api/Features/Projects/Requests/Import.cs b/src/Caster.Api/Features/Projects/Requests/Import.cs index f26327e..7d1c6fa 100644 --- a/src/Caster.Api/Features/Projects/Requests/Import.cs +++ b/src/Caster.Api/Features/Projects/Requests/Import.cs @@ -10,26 +10,21 @@ using Microsoft.EntityFrameworkCore; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using System.Text.Json.Serialization; using Caster.Api.Domain.Services; using Microsoft.AspNetCore.Http; using FluentValidation; -using System.Linq; using Caster.Api.Domain.Models; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.ChangeTracking; -using Caster.Api.Domain.Events; using Caster.Api.Data.Extensions; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Projects { public class Import { - [DataContract(Name="ImportProjectCommand")] + [DataContract(Name = "ImportProjectCommand")] public class Command : IRequest { [JsonIgnore] @@ -76,40 +71,20 @@ public class ImportProjectResult public string[] LockedFiles { get; set; } } - public class Handler : IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + IArchiveService archiveService, + IImportService importService, + IMediator mediator) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly IArchiveService _archiveService; - private readonly IImportService _importService; - private readonly IMediator _mediator; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - IArchiveService archiveService, - IImportService importService, - IMediator mediator) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _archiveService = archiveService; - _importService = importService; - _mediator = mediator; - } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ImportProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var project = await _db.Projects + var project = await dbContext.Projects .Include(e => e.Directories) .ThenInclude(d => d.Workspaces) .Include(e => e.Directories) @@ -125,16 +100,16 @@ public async Task Handle(Command request, CancellationToken { await request.Archive.CopyToAsync(memStream, cancellationToken); memStream.Position = 0; - extractedProject = _archiveService.ExtractProject(memStream, request.Archive.FileName); + extractedProject = archiveService.ExtractProject(memStream, request.Archive.FileName); } - var importResult = await _importService.ImportProject(project, extractedProject, request.PreserveIds); + var importResult = await importService.ImportProject(project, extractedProject, request.PreserveIds); - var entries = _db.GetUpdatedEntries(); - await _db.SaveChangesAsync(cancellationToken); + var entries = dbContext.GetUpdatedEntries(); + await dbContext.SaveChangesAsync(cancellationToken); await this.PublishEvents(entries); - return _mapper.Map(importResult); + return mapper.Map(importResult); } private async Task PublishEvents(EntityEntry[] entries) @@ -145,7 +120,7 @@ private async Task PublishEvents(EntityEntry[] entries) if (evt != null) { - await _mediator.Publish(evt); + await mediator.Publish(evt); } } } diff --git a/src/Caster.Api/Features/Resources/Commands/BaseOperation.cs b/src/Caster.Api/Features/Resources/Commands/BaseOperation.cs index eb23327..9a053de 100644 --- a/src/Caster.Api/Features/Resources/Commands/BaseOperation.cs +++ b/src/Caster.Api/Features/Resources/Commands/BaseOperation.cs @@ -5,22 +5,26 @@ using AutoMapper; using Caster.Api.Data; using Caster.Api.Domain.Models; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Options; using Caster.Api.Domain.Services; using Caster.Api.Infrastructure.Exceptions; -using Microsoft.Extensions.Logging; -using Caster.Api.Infrastructure.Identity; using System.Collections.Generic; using Microsoft.EntityFrameworkCore; using System; using System.Linq; using System.Text.Json; +using Caster.Api.Features.Shared; +using MediatR; namespace Caster.Api.Features.Resources { - public class BaseOperationHandler + public abstract class BaseOperationHandler( + IMapper mapper, + CasterContext dbContext, + ILockService lockService, + TerraformOptions terraformOptions, + ITerraformService terraformService) : BaseHandler + where TRequest : IRequest { protected enum ResourceOperation { @@ -32,43 +36,14 @@ protected enum ResourceOperation output } - protected readonly CasterContext _db; - protected readonly IMapper _mapper; - protected readonly IAuthorizationService _authorizationService; - protected readonly ClaimsPrincipal _user; - protected readonly TerraformOptions _terraformOptions; - protected readonly ITerraformService _terraformService; - protected readonly ILockService _lockService; - protected readonly ILogger _logger; - - public BaseOperationHandler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - TerraformOptions terraformOptions, - ITerraformService terraformService, - ILockService lockService, - ILogger logger) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _terraformOptions = terraformOptions; - _terraformService = terraformService; - _lockService = lockService; - _logger = logger; - } - protected async Task PerformOperation(Workspace workspace, ResourceOperation operation, string[] addresses, string args = null) { - using (var lockResult = await _lockService.GetWorkspaceLock(workspace.Id).LockAsync(0)) + using (var lockResult = await lockService.GetWorkspaceLock(workspace.Id).LockAsync(0)) { if (!lockResult.AcquiredLock) throw new WorkspaceConflictException(); - if (!await _db.AnyIncompleteRuns(workspace.Id)) + if (!await dbContext.AnyIncompleteRuns(workspace.Id)) { return await this.OperationDoWork(workspace, operation, addresses, args); } @@ -83,11 +58,11 @@ private async Task OperationDoWork(Workspace workspace, R { var errors = new List(); JsonElement? outputs = null; - var workingDir = workspace.GetPath(_terraformOptions.RootWorkingDirectory); - var files = await _db.GetWorkspaceFiles(workspace, workspace.Directory); + var workingDir = workspace.GetPath(terraformOptions.RootWorkingDirectory); + var files = await dbContext.GetWorkspaceFiles(workspace, workspace.Directory); await workspace.PrepareFileSystem(workingDir, files); - var initResult = _terraformService.InitializeWorkspace(workspace, null); + var initResult = terraformService.InitializeWorkspace(workspace, null); var statePath = string.Empty; @@ -111,10 +86,10 @@ private async Task OperationDoWork(Workspace workspace, R switch (operation) { case ResourceOperation.taint: - taintResult = _terraformService.Taint(workspace, address, statePath); + taintResult = terraformService.Taint(workspace, address, statePath); break; case ResourceOperation.untaint: - taintResult = _terraformService.Untaint(workspace, address, statePath); + taintResult = terraformService.Untaint(workspace, address, statePath); break; } @@ -125,16 +100,16 @@ private async Task OperationDoWork(Workspace workspace, R } break; case ResourceOperation.refresh: - result = _terraformService.Refresh(workspace, statePath); + result = terraformService.Refresh(workspace, statePath); break; case ResourceOperation.remove: - result = _terraformService.RemoveResources(workspace, addresses, statePath); + result = terraformService.RemoveResources(workspace, addresses, statePath); break; case ResourceOperation.import: - result = _terraformService.Import(workspace, addresses[0], args, statePath); + result = terraformService.Import(workspace, addresses[0], args, statePath); break; case ResourceOperation.output: - result = _terraformService.GetOutputs(workspace, statePath); + result = terraformService.GetOutputs(workspace, statePath); outputs = JsonDocument.Parse(result.Output).RootElement; break; } @@ -145,13 +120,13 @@ private async Task OperationDoWork(Workspace workspace, R } await workspace.RetrieveState(workingDir); - await _db.SaveChangesAsync(); - workspace.CleanupFileSystem(_terraformOptions.RootWorkingDirectory); + await dbContext.SaveChangesAsync(); + workspace.CleanupFileSystem(terraformOptions.RootWorkingDirectory); } return new ResourceCommandResult { - Resources = _mapper.Map(workspace.GetState().GetResources(), opts => opts.ExcludeMembers(nameof(Resource.Attributes))), + Resources = mapper.Map(workspace.GetState().GetResources(), opts => opts.ExcludeMembers(nameof(Resource.Attributes))), Errors = errors.ToArray(), Outputs = outputs }; @@ -159,7 +134,7 @@ private async Task OperationDoWork(Workspace workspace, R protected async Task GetWorkspace(Guid id) { - var workspace = await _db.Workspaces + var workspace = await dbContext.Workspaces .Include(w => w.Directory) .Where(w => w.Id == id) .FirstOrDefaultAsync(); diff --git a/src/Caster.Api/Features/Resources/Commands/Import.cs b/src/Caster.Api/Features/Resources/Commands/Import.cs index a4c1bd3..6d4cb4e 100644 --- a/src/Caster.Api/Features/Resources/Commands/Import.cs +++ b/src/Caster.Api/Features/Resources/Commands/Import.cs @@ -46,31 +46,25 @@ public class Command : IRequest public string ResourceId { get; set; } } - public class Handler : BaseOperationHandler, IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + ILockService lockService, + TerraformOptions terraformOptions, + ITerraformService terraformService) : BaseOperationHandler(mapper, dbContext, lockService, terraformOptions, terraformService) { - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - TerraformOptions terraformOptions, - ITerraformService terraformService, - ILockService lockService, - ILogger logger) : - base(db, mapper, authorizationService, identityResolver, terraformOptions, terraformService, lockService, logger) - { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ImportResources], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - var workspace = await base.GetWorkspace(request.WorkspaceId); return await base.PerformOperation( workspace, ResourceOperation.import, - new string[] { request.ResourceAddress }, + [request.ResourceAddress], request.ResourceId); } } diff --git a/src/Caster.Api/Features/Resources/Commands/Output.cs b/src/Caster.Api/Features/Resources/Commands/Output.cs index 1b4451c..e698c92 100644 --- a/src/Caster.Api/Features/Resources/Commands/Output.cs +++ b/src/Caster.Api/Features/Resources/Commands/Output.cs @@ -9,15 +9,9 @@ using Caster.Api.Data; using System; using Caster.Api.Domain.Models; -using Caster.Api.Infrastructure.Exceptions; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Microsoft.EntityFrameworkCore; -using System.Linq; using Caster.Api.Infrastructure.Options; using Caster.Api.Domain.Services; -using Microsoft.Extensions.Logging; -using Caster.Api.Infrastructure.Identity; using System.Text.Json.Serialization; namespace Caster.Api.Features.Resources @@ -34,27 +28,20 @@ public class Command : IRequest public Guid WorkspaceId { get; set; } } - public class Handler : BaseOperationHandler, IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + ILockService lockService, + TerraformOptions terraformOptions, + ITerraformService terraformService) : BaseOperationHandler(mapper, dbContext, lockService, terraformOptions, terraformService) { - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - TerraformOptions terraformOptions, - ITerraformService terraformService, - ILockService lockService, - ILogger logger) : - base(db, mapper, authorizationService, identityResolver, terraformOptions, terraformService, lockService, logger) - { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.WorkspaceId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - var workspace = await base.GetWorkspace(request.WorkspaceId); - return await base.PerformOperation(workspace, ResourceOperation.output, null); } } diff --git a/src/Caster.Api/Features/Resources/Commands/Refresh.cs b/src/Caster.Api/Features/Resources/Commands/Refresh.cs index c0c0183..c8a1331 100644 --- a/src/Caster.Api/Features/Resources/Commands/Refresh.cs +++ b/src/Caster.Api/Features/Resources/Commands/Refresh.cs @@ -9,15 +9,9 @@ using Caster.Api.Data; using System; using Caster.Api.Domain.Models; -using Caster.Api.Infrastructure.Exceptions; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Microsoft.EntityFrameworkCore; -using System.Linq; using Caster.Api.Infrastructure.Options; using Caster.Api.Domain.Services; -using Microsoft.Extensions.Logging; -using Caster.Api.Infrastructure.Identity; using System.Text.Json.Serialization; namespace Caster.Api.Features.Resources @@ -34,27 +28,20 @@ public class Command : IRequest public Guid WorkspaceId { get; set; } } - public class Handler : BaseOperationHandler, IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + ILockService lockService, + TerraformOptions terraformOptions, + ITerraformService terraformService) : BaseOperationHandler(mapper, dbContext, lockService, terraformOptions, terraformService) { - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - TerraformOptions terraformOptions, - ITerraformService terraformService, - ILockService lockService, - ILogger logger) : - base(db, mapper, authorizationService, identityResolver, terraformOptions, terraformService, lockService, logger) - { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.WorkspaceId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - var workspace = await base.GetWorkspace(request.WorkspaceId); - return await base.PerformOperation(workspace, ResourceOperation.refresh, null); } } diff --git a/src/Caster.Api/Features/Resources/Commands/Remove.cs b/src/Caster.Api/Features/Resources/Commands/Remove.cs index 84878c0..58cc90f 100644 --- a/src/Caster.Api/Features/Resources/Commands/Remove.cs +++ b/src/Caster.Api/Features/Resources/Commands/Remove.cs @@ -40,25 +40,19 @@ public class Command : IRequest public string[] ResourceAddresses { get; set; } } - public class Handler : BaseOperationHandler, IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + ILockService lockService, + TerraformOptions terraformOptions, + ITerraformService terraformService) : BaseOperationHandler(mapper, dbContext, lockService, terraformOptions, terraformService) { - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - TerraformOptions terraformOptions, - ITerraformService terraformService, - ILockService lockService, - ILogger logger) : - base(db, mapper, authorizationService, identityResolver, terraformOptions, terraformService, lockService, logger) - { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.WorkspaceId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - var workspace = await base.GetWorkspace(request.WorkspaceId); return await base.PerformOperation( diff --git a/src/Caster.Api/Features/Resources/Commands/Taint.cs b/src/Caster.Api/Features/Resources/Commands/Taint.cs index 8e6184b..df1e1c9 100644 --- a/src/Caster.Api/Features/Resources/Commands/Taint.cs +++ b/src/Caster.Api/Features/Resources/Commands/Taint.cs @@ -9,15 +9,10 @@ using Caster.Api.Data; using System; using Caster.Api.Domain.Models; -using Caster.Api.Infrastructure.Exceptions; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Microsoft.EntityFrameworkCore; using System.Linq; using Caster.Api.Infrastructure.Options; using Caster.Api.Domain.Services; -using Microsoft.Extensions.Logging; -using Caster.Api.Infrastructure.Identity; using System.Text.Json.Serialization; namespace Caster.Api.Features.Resources @@ -52,25 +47,19 @@ public class Command : IRequest public string[] ResourceAddresses { get; set; } } - public class Handler : BaseOperationHandler, IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + ILockService lockService, + TerraformOptions terraformOptions, + ITerraformService terraformService) : BaseOperationHandler(mapper, dbContext, lockService, terraformOptions, terraformService) { - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - TerraformOptions terraformOptions, - ITerraformService terraformService, - ILockService lockService, - ILogger logger) : - base(db, mapper, authorizationService, identityResolver, terraformOptions, terraformService, lockService, logger) - { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.WorkspaceId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - var workspace = await base.GetWorkspace(request.WorkspaceId); string[] addresses = request.ResourceAddresses; diff --git a/src/Caster.Api/Features/Resources/Queries/Get.cs b/src/Caster.Api/Features/Resources/Queries/Get.cs index 48da8b0..c49dfbc 100644 --- a/src/Caster.Api/Features/Resources/Queries/Get.cs +++ b/src/Caster.Api/Features/Resources/Queries/Get.cs @@ -10,14 +10,11 @@ using System; using Caster.Api.Domain.Models; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using System.Linq; -using Caster.Api.Infrastructure.Identity; -using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using System.Web; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Resources { @@ -39,31 +36,14 @@ public class Query : IRequest public string Address { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.WorkspaceId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var workspace = await _db.Workspaces.FindAsync(new object[] { request.WorkspaceId }, cancellationToken); + var workspace = await dbContext.Workspaces.FindAsync([request.WorkspaceId], cancellationToken); if (workspace == null) throw new EntityNotFoundException(); @@ -74,7 +54,7 @@ public async Task Handle(Query request, CancellationToken cancellation var address = HttpUtility.UrlDecode(request.Address); var resource = resources.Where(r => r.Address == address).FirstOrDefault(); - return _mapper.Map(resource, opts => opts.ExcludeMembers()); + return mapper.Map(resource, opts => opts.ExcludeMembers()); } } } diff --git a/src/Caster.Api/Features/Resources/Queries/GetByWorkspace.cs b/src/Caster.Api/Features/Resources/Queries/GetByWorkspace.cs index 8360a73..b8d960d 100644 --- a/src/Caster.Api/Features/Resources/Queries/GetByWorkspace.cs +++ b/src/Caster.Api/Features/Resources/Queries/GetByWorkspace.cs @@ -15,6 +15,7 @@ using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Identity; using System.Text.Json.Serialization; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Resources { @@ -30,31 +31,14 @@ public class Query : IRequest public Guid WorkspaceId { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.WorkspaceId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var workspace = await _db.Workspaces.FindAsync(request.WorkspaceId); + var workspace = await dbContext.Workspaces.FindAsync(request.WorkspaceId); if (workspace == null) { @@ -62,7 +46,7 @@ public async Task Handle(Query request, CancellationToken cancellati } var state = workspace.GetState(); - return _mapper.Map(state.GetResources(), opts => opts.ExcludeMembers(nameof(Resource.Attributes))); + return mapper.Map(state.GetResources(), opts => opts.ExcludeMembers(nameof(Resource.Attributes))); } } } diff --git a/src/Caster.Api/Features/Runs/Requests/Cancel.cs b/src/Caster.Api/Features/Runs/Requests/Cancel.cs index 58f8171..8b811c7 100644 --- a/src/Caster.Api/Features/Runs/Requests/Cancel.cs +++ b/src/Caster.Api/Features/Runs/Requests/Cancel.cs @@ -11,18 +11,12 @@ using System.Runtime.Serialization; using Caster.Api.Domain.Models; using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Options; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Domain.Services; using System.Linq; -using Caster.Api.Infrastructure.Identity; -using Caster.Api.Domain.Events; using AutoMapper.QueryableExtensions; -using Caster.Api.Infrastructure.Extensions; using System.Text.Json.Serialization; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Runs { @@ -45,44 +39,28 @@ public class Command : IRequest public bool Force { get; set; } } - public class Handler : IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + ITerraformService terraformService) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly ITerraformService _terraformService; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ITerraformService terraformService) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _terraformService = terraformService; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var run = await _db.Runs + var run = await dbContext.Runs .Where(x => x.Id == request.Id) .Include(x => x.Workspace) .SingleOrDefaultAsync(cancellationToken); ValidateRun(run); - _terraformService.CancelRun(run.Workspace, request.Force); + terraformService.CancelRun(run.Workspace, request.Force); - return await _db.Runs - .ProjectTo(_mapper.ConfigurationProvider) + return await dbContext.Runs + .ProjectTo(mapper.ConfigurationProvider) .SingleOrDefaultAsync(x => x.Id == request.Id, cancellationToken); } diff --git a/src/Caster.Api/Features/Runs/Requests/Create.cs b/src/Caster.Api/Features/Runs/Requests/Create.cs index 0fd3e09..d3d5c2f 100644 --- a/src/Caster.Api/Features/Runs/Requests/Create.cs +++ b/src/Caster.Api/Features/Runs/Requests/Create.cs @@ -8,19 +8,18 @@ using Caster.Api.Data; using AutoMapper; using System.Runtime.Serialization; -using System.Linq; using Caster.Api.Domain.Models; using Microsoft.EntityFrameworkCore; using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Events; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Domain.Services; using Caster.Api.Infrastructure.Identity; using AutoMapper.QueryableExtensions; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using FluentValidation; +using Caster.Api.Features.Shared.Services; namespace Caster.Api.Features.Runs { @@ -54,77 +53,58 @@ public class Command : IRequest public string[] ReplaceAddresses { get; set; } } - public class Handler : IRequestHandler + public class CommandValidator : AbstractValidator { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IMediator _mediator; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly ILockService _lockService; - - public Handler( - CasterContext db, - IMapper mapper, - IMediator mediator, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService) + public CommandValidator(IValidationService validationService) { - _db = db; - _mapper = mapper; - _mediator = mediator; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _lockService = lockService; + RuleFor(x => x.WorkspaceId).WorkspaceExists(validationService); } + } - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - await ValidateWorkspace(request.WorkspaceId); + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + IMediator mediator, + ILockService lockService, + IIdentityResolver identityResolver) : BaseHandler + { + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.WorkspaceId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) + { Domain.Models.Run run = null; - using (var lockResult = await _lockService.GetWorkspaceLock(request.WorkspaceId).LockAsync(0)) + using (var lockResult = await lockService.GetWorkspaceLock(request.WorkspaceId).LockAsync(0)) { if (!lockResult.AcquiredLock) throw new WorkspaceConflictException(); - if ((await _db.AnyIncompleteRuns(request.WorkspaceId))) + if (await dbContext.AnyIncompleteRuns(request.WorkspaceId)) { throw new ConflictException("This Workspace's current Run must be rejected or applied before a new one can be created."); } - run = await this.DoWork(request); + run = await this.DoWork(request, identityResolver.GetClaimsPrincipal().GetId(), cancellationToken); } - await _mediator.Publish(new RunCreated { RunId = run.Id }); + await mediator.Publish(new RunCreated { RunId = run.Id }); - return await _db.Runs - .ProjectTo(_mapper.ConfigurationProvider) + return await dbContext.Runs + .ProjectTo(mapper.ConfigurationProvider) .SingleOrDefaultAsync(x => x.Id == run.Id, cancellationToken); } - private async Task DoWork(Command request) + private async Task DoWork(Command request, Guid userId, CancellationToken cancellationToken) { - var run = _mapper.Map(request); - run.CreatedById = _user.GetId(); - run.Modify(_user.GetId()); - await _db.Runs.AddAsync(run); - await _db.SaveChangesAsync(); + var run = mapper.Map(request); + run.CreatedById = userId; + run.Modify(userId); + dbContext.Runs.Add(run); + await dbContext.SaveChangesAsync(cancellationToken); return run; } - - private async Task ValidateWorkspace(Guid workspaceId) - { - var workspace = await _db.Workspaces.FindAsync(workspaceId); - - if (workspace == null) - throw new EntityNotFoundException(); - } } } } diff --git a/src/Caster.Api/Features/Runs/Requests/Get.cs b/src/Caster.Api/Features/Runs/Requests/Get.cs index c3d7c16..1fbf17b 100644 --- a/src/Caster.Api/Features/Runs/Requests/Get.cs +++ b/src/Caster.Api/Features/Runs/Requests/Get.cs @@ -10,17 +10,16 @@ using Microsoft.EntityFrameworkCore; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using System.Text.Json.Serialization; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Runs { public class Get { - [DataContract(Name="GetRunQuery")] + [DataContract(Name = "GetRunQuery")] public class Query : RunQuery, IRequest { /// @@ -30,32 +29,15 @@ public class Query : RunQuery, IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var run = await _db.Runs - .Expand(_mapper.ConfigurationProvider, + var run = await dbContext.Runs + .Expand(mapper.ConfigurationProvider, includePlan: request.IncludePlan, includeApply: request.IncludeApply) .SingleOrDefaultAsync(x => x.Id == request.Id, cancellationToken); diff --git a/src/Caster.Api/Features/Runs/Requests/GetAll.cs b/src/Caster.Api/Features/Runs/Requests/GetAll.cs index 998f75f..5cc02c2 100644 --- a/src/Caster.Api/Features/Runs/Requests/GetAll.cs +++ b/src/Caster.Api/Features/Runs/Requests/GetAll.cs @@ -10,18 +10,15 @@ using System.Linq; using Microsoft.EntityFrameworkCore; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Models; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Runs { public class GetAll { - [DataContract(Name="GetAllRunsQuery")] + [DataContract(Name = "GetAllRunsQuery")] public class Query : RunQuery, IRequest { /// @@ -35,34 +32,17 @@ public class Query : RunQuery, IRequest public int? Limit { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewProjects, SystemPermission.ViewWorkspaces], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var query = _db.Runs + var query = dbContext.Runs .OrderByDescending(r => r.CreatedAt) .Limit(request.Limit) - .Expand(_mapper.ConfigurationProvider, + .Expand(mapper.ConfigurationProvider, includePlan: request.IncludePlan, includeApply: request.IncludeApply); @@ -71,7 +51,7 @@ public async Task Handle(Query request, CancellationToken cancellationTok query = query.Where(x => RunHelpers.GetActiveStatuses().Contains(x.Status)); } - return await query.ToArrayAsync(); + return await query.ToArrayAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Runs/Requests/GetByWorkspace.cs b/src/Caster.Api/Features/Runs/Requests/GetByWorkspace.cs index ce95540..f45f447 100644 --- a/src/Caster.Api/Features/Runs/Requests/GetByWorkspace.cs +++ b/src/Caster.Api/Features/Runs/Requests/GetByWorkspace.cs @@ -9,22 +9,20 @@ using Caster.Api.Data; using System.Linq; using Microsoft.EntityFrameworkCore; -using AutoMapper.QueryableExtensions; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Models; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using System.Text.Json.Serialization; +using Caster.Api.Features.Shared; +using FluentValidation; +using Caster.Api.Features.Shared.Services; +using Caster.Api.Infrastructure.Extensions; namespace Caster.Api.Features.Runs { public class GetByWorkspace { - [DataContract(Name="GetRunsByWorkspaceQuery")] + [DataContract(Name = "GetRunsByWorkspaceQuery")] public class Query : RunQuery, IRequest { /// @@ -39,48 +37,29 @@ public class Query : RunQuery, IRequest public int? Limit { get; set; } } - public class Handler : IRequestHandler + public class RequestValidator : AbstractValidator { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public RequestValidator(IValidationService validationService) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); + RuleFor(x => x.WorkspaceId).WorkspaceExists(validationService); } + } - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - await ValidateWorkspace(request.WorkspaceId); + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.WorkspaceId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - return await _db.Runs + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { + return await dbContext.Runs .Where(x => x.WorkspaceId == request.WorkspaceId) .OrderByDescending(r => r.CreatedAt) .Limit(request.Limit) - .Expand(_mapper.ConfigurationProvider, + .Expand(mapper.ConfigurationProvider, includePlan: request.IncludePlan, includeApply: request.IncludeApply) - .ToArrayAsync(); - } - - private async Task ValidateWorkspace(Guid workspaceId) - { - var workspace = await _db.Workspaces.FindAsync(workspaceId); - - if (workspace == null) - throw new EntityNotFoundException(); + .ToArrayAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Runs/Requests/Reject.cs b/src/Caster.Api/Features/Runs/Requests/Reject.cs index 9cf998e..50ba143 100644 --- a/src/Caster.Api/Features/Runs/Requests/Reject.cs +++ b/src/Caster.Api/Features/Runs/Requests/Reject.cs @@ -12,16 +12,13 @@ using Caster.Api.Domain.Models; using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Infrastructure.Options; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Domain.Services; using System.Linq; using Caster.Api.Infrastructure.Identity; using Caster.Api.Domain.Events; using AutoMapper.QueryableExtensions; -using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Runs { @@ -37,47 +34,31 @@ public class Command : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + TerraformOptions terraformOptions, + ILockService lockService, + IMediator mediator, + IIdentityResolver identityResolver) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly TerraformOptions _terraformOptions; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly ILockService _lockService; - private readonly IMediator _mediator; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - TerraformOptions terraformOptions, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService, - IMediator mediator) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _terraformOptions = terraformOptions; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _lockService = lockService; - _mediator = mediator; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var workspaceId = await _db.Runs.Where(r => r.Id == request.Id).Select(r => r.WorkspaceId).FirstOrDefaultAsync(); + var workspaceId = await dbContext.Runs + .Where(r => r.Id == request.Id) + .Select(r => r.WorkspaceId) + .FirstOrDefaultAsync(cancellationToken); - using (var lockResult = await _lockService.GetWorkspaceLock(workspaceId).LockAsync(0)) + using (var lockResult = await lockService.GetWorkspaceLock(workspaceId).LockAsync(0)) { if (!lockResult.AcquiredLock) throw new WorkspaceConflictException(); - var run = await _db.Runs + var run = await dbContext.Runs .Include(r => r.Plan) .Include(r => r.Apply) .Include(r => r.Workspace) @@ -85,7 +66,7 @@ public async Task Handle(Command request, CancellationToken cancellationTok ValidateRun(run); - run.Workspace.CleanupFileSystem(_terraformOptions.RootWorkingDirectory); + run.Workspace.CleanupFileSystem(terraformOptions.RootWorkingDirectory); run.Status = RunStatus.Rejected; @@ -94,12 +75,12 @@ public async Task Handle(Command request, CancellationToken cancellationTok run.Plan.Status = PlanStatus.Rejected; } - run.Modify(_user.GetId()); - await _db.SaveChangesAsync(); - await _mediator.Publish(new RunUpdated(run.Id)); + run.Modify(identityResolver.GetId()); + await dbContext.SaveChangesAsync(); + await mediator.Publish(new RunUpdated(run.Id)); - return await _db.Runs - .ProjectTo(_mapper.ConfigurationProvider) + return await dbContext.Runs + .ProjectTo(mapper.ConfigurationProvider) .SingleOrDefaultAsync(x => x.Id == run.Id, cancellationToken); } } diff --git a/src/Caster.Api/Features/Runs/Requests/SaveState.cs b/src/Caster.Api/Features/Runs/Requests/SaveState.cs index 549debf..1da62f9 100644 --- a/src/Caster.Api/Features/Runs/Requests/SaveState.cs +++ b/src/Caster.Api/Features/Runs/Requests/SaveState.cs @@ -12,15 +12,13 @@ using Caster.Api.Domain.Models; using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Events; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Domain.Services; using System.Linq; using Caster.Api.Infrastructure.Identity; using Caster.Api.Infrastructure.Options; using AutoMapper.QueryableExtensions; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Runs { @@ -36,40 +34,20 @@ public class Command : IRequest public Guid RunId { get; set; } } - public class Handler : IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + TerraformOptions terraformOptions, + ILockService lockService, + IMediator mediator) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IMediator _mediator; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly ILockService _lockService; - private readonly TerraformOptions _options; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.RunId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IMediator mediator, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService, - TerraformOptions options) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _mediator = mediator; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _lockService = lockService; - _options = options; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var run = await _db.Runs + var run = await dbContext.Runs .Include(r => r.Apply) .Include(r => r.Workspace) .Where(r => r.Id == request.RunId) @@ -78,14 +56,14 @@ public async Task Handle(Command request, CancellationToken cancellationTok if (run == null) throw new EntityNotFoundException(); - using (var lockResult = await _lockService.GetWorkspaceLock(run.WorkspaceId).LockAsync(0)) + using (var lockResult = await lockService.GetWorkspaceLock(run.WorkspaceId).LockAsync(0)) { if (!lockResult.AcquiredLock) throw new WorkspaceConflictException(); await ValidateRun(run, cancellationToken); - var workingDir = run.Workspace.GetPath(_options.RootWorkingDirectory); + var workingDir = run.Workspace.GetPath(terraformOptions.RootWorkingDirectory); var stateRetrieved = await run.Workspace.RetrieveState(workingDir); if (stateRetrieved) @@ -93,15 +71,15 @@ public async Task Handle(Command request, CancellationToken cancellationTok run.Apply.Status = run.Apply.Status == ApplyStatus.Applied_StateError ? ApplyStatus.Applied : ApplyStatus.Failed; run.Status = run.Status == RunStatus.Applied_StateError ? RunStatus.Applied : RunStatus.Failed; - await _db.SaveChangesAsync(cancellationToken); - await _mediator.Publish(new RunUpdated(run.Id)); - await _mediator.Publish(new ApplyCompleted(run.Workspace)); - run.Workspace.CleanupFileSystem(_options.RootWorkingDirectory); + await dbContext.SaveChangesAsync(cancellationToken); + await mediator.Publish(new RunUpdated(run.Id)); + await mediator.Publish(new ApplyCompleted(run.Workspace)); + run.Workspace.CleanupFileSystem(terraformOptions.RootWorkingDirectory); } } - return await _db.Runs - .ProjectTo(_mapper.ConfigurationProvider) + return await dbContext.Runs + .ProjectTo(mapper.ConfigurationProvider) .SingleOrDefaultAsync(x => x.Id == run.Id, cancellationToken); } @@ -110,7 +88,7 @@ private async Task ValidateRun(Domain.Models.Run run, CancellationToken cancella if (run.Status != RunStatus.Applied_StateError && run.Status != RunStatus.Failed_StateError) throw new WorkspaceConflictException(); - var notLatest = await _db.Runs + var notLatest = await dbContext.Runs .AnyAsync(r => r.WorkspaceId == run.WorkspaceId && r.CreatedAt > run.CreatedAt, diff --git a/src/Caster.Api/Features/Shared/BaseHandler.cs b/src/Caster.Api/Features/Shared/BaseHandler.cs index e8e0139..0351a29 100644 --- a/src/Caster.Api/Features/Shared/BaseHandler.cs +++ b/src/Caster.Api/Features/Shared/BaseHandler.cs @@ -1,61 +1,39 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. -using AutoMapper; -using Caster.Api.Data; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; -using Microsoft.Extensions.Logging; -using Caster.Api.Infrastructure.Identity; +using MediatR; +using System.Threading.Tasks; +using System.Threading; +using Caster.Api.Infrastructure.Exceptions; namespace Caster.Api.Features.Shared; -public interface IDependencyAggregate +public abstract class BaseHandler : IRequestHandler + where TRequest : IRequest { - CasterContext DbContext { get; } - IMapper Mapper { get; } - IAuthorizationService AuthorizationService { get; } - IIdentityResolver IdentityResolver { get; } - ILogger Logger { get; } -} - -public class DependencyAggregate : IDependencyAggregate -{ - public DependencyAggregate( - CasterContext dbContext, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILogger logger) + public async Task Handle(TRequest request, CancellationToken cancellationToken) { - DbContext = dbContext; - Mapper = mapper; - AuthorizationService = authorizationService; - IdentityResolver = identityResolver; - Logger = logger; + if (!await this.Authorize(request, cancellationToken)) + throw new ForbiddenException(); + + return await this.HandleRequest(request, cancellationToken); } - public CasterContext DbContext { get; } - public IMapper Mapper { get; } - public IAuthorizationService AuthorizationService { get; } - public IIdentityResolver IdentityResolver { get; } - public ILogger Logger { get; } + public abstract Task Authorize(TRequest request, CancellationToken cancellationToken); + public abstract Task HandleRequest(TRequest request, CancellationToken cancellationToken); } -public class BaseHandler +public abstract class BaseHandler : IRequestHandler + where TRequest : IRequest { - protected readonly CasterContext _db; - protected readonly IMapper _mapper; - protected readonly IAuthorizationService _authorizationService; - protected readonly ClaimsPrincipal _user; - protected readonly ILogger _logger; - - public BaseHandler(IDependencyAggregate aggregate) + public async Task Handle(TRequest request, CancellationToken cancellationToken) { - _db = aggregate.DbContext; - _mapper = aggregate.Mapper; - _authorizationService = aggregate.AuthorizationService; - _user = aggregate.IdentityResolver.GetClaimsPrincipal(); - _logger = aggregate.Logger; + if (!await this.Authorize(request, cancellationToken)) + throw new ForbiddenException(); + + await this.HandleRequest(request, cancellationToken); } -} + + public abstract Task Authorize(TRequest request, CancellationToken cancellationToken); + public abstract Task HandleRequest(TRequest request, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Caster.Api/Features/Shared/Services/ValidationService.cs b/src/Caster.Api/Features/Shared/Services/ValidationService.cs index 9a6934d..0fc4480 100644 --- a/src/Caster.Api/Features/Shared/Services/ValidationService.cs +++ b/src/Caster.Api/Features/Shared/Services/ValidationService.cs @@ -18,6 +18,11 @@ public interface IValidationService Task PoolExists(Guid poolId); Task WorkspaceExists(Guid workspaceId); Task VlanExists(Guid vlanId); + Task UserExists(Guid userId); + Task GroupExists(Guid groupId); + Task ProjectRoleExists(Guid roleId); + Task SystemRoleExists(Guid roleId); + Task RunExists(Guid runId); } public class ValidationService : IValidationService @@ -68,4 +73,29 @@ public async Task VlanExists(Guid vlanId) { return await _dbContext.Vlans.AnyAsync(x => x.Id == vlanId); } + + public async Task UserExists(Guid userId) + { + return await _dbContext.Users.AnyAsync(x => x.Id == userId); + } + + public async Task GroupExists(Guid groupId) + { + return await _dbContext.Groups.AnyAsync(x => x.Id == groupId); + } + + public async Task ProjectRoleExists(Guid roleId) + { + return await _dbContext.ProjectRoles.AnyAsync(x => x.Id == roleId); + } + + public async Task SystemRoleExists(Guid roleId) + { + return await _dbContext.SystemRoles.AnyAsync(x => x.Id == roleId); + } + + public async Task RunExists(Guid runId) + { + return await _dbContext.Runs.AnyAsync(x => x.Id == runId); + } } diff --git a/src/Caster.Api/Features/Workspaces/Interfaces/IWorkspaceDeleteRequest.cs b/src/Caster.Api/Features/SystemPermissions/MappingProfile.cs similarity index 53% rename from src/Caster.Api/Features/Workspaces/Interfaces/IWorkspaceDeleteRequest.cs rename to src/Caster.Api/Features/SystemPermissions/MappingProfile.cs index 604bab5..de87393 100644 --- a/src/Caster.Api/Features/Workspaces/Interfaces/IWorkspaceDeleteRequest.cs +++ b/src/Caster.Api/Features/SystemPermissions/MappingProfile.cs @@ -1,13 +1,14 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. -using System; - -namespace Caster.Api.Features.Workspaces.Interfaces +namespace Caster.Api.Features.SystemPermissions { - public interface IWorkspaceDeleteRequest + using AutoMapper; + + public class MappingProfile : Profile { - Guid Id { get; set; } + public MappingProfile() + { + } } } - diff --git a/src/Caster.Api/Features/SystemPermissions/Requests/GetMine.cs b/src/Caster.Api/Features/SystemPermissions/Requests/GetMine.cs new file mode 100644 index 0000000..c370239 --- /dev/null +++ b/src/Caster.Api/Features/SystemPermissions/Requests/GetMine.cs @@ -0,0 +1,32 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using System.Runtime.Serialization; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Features.Shared; +using System.Linq; + +namespace Caster.Api.Features.SystemPermissions +{ + public class GetMine + { + [DataContract(Name = "GetMySystemPermissionsQuery")] + public class Query : IRequest + { + } + + public class Handler(ICasterAuthorizationService authorizationService) : BaseHandler + { + public override Task Authorize(Query request, CancellationToken cancellationToken) => Task.FromResult(true); + + public override Task HandleRequest(Query request, CancellationToken cancellationToken) + { + return Task.FromResult(authorizationService.GetSystemPermissions().ToArray()); + } + } + } +} + diff --git a/src/Caster.Api/Features/SystemPermissions/SystemPermissionsController.cs b/src/Caster.Api/Features/SystemPermissions/SystemPermissionsController.cs new file mode 100644 index 0000000..0ab3ede --- /dev/null +++ b/src/Caster.Api/Features/SystemPermissions/SystemPermissionsController.cs @@ -0,0 +1,38 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Caster.Api.Features.SystemPermissions; + +[Route("api/permissions")] +[ApiController] +[Authorize] +public class SystemPermissionsController : ControllerBase +{ + private readonly IMediator _mediator; + + public SystemPermissionsController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// Get all SystemPermissions for the calling User. + /// + /// + [HttpGet("mine")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "GetMySystemPermissions")] + public async Task GetAll() + { + var result = await _mediator.Send(new GetMine.Query()); + return Ok(result); + } +} \ No newline at end of file diff --git a/src/Caster.Api/Features/SystemRoles/EventHandlers/AuthCacheEventHandler.cs b/src/Caster.Api/Features/SystemRoles/EventHandlers/AuthCacheEventHandler.cs new file mode 100644 index 0000000..bfcab6b --- /dev/null +++ b/src/Caster.Api/Features/SystemRoles/EventHandlers/AuthCacheEventHandler.cs @@ -0,0 +1,53 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Caster.Api.Data; +using Caster.Api.Domain.Events; +using Caster.Api.Domain.Models; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; + +namespace Caster.Api.Features.Projects.EventHandlers; + +public class SystemRoleBaseAuthCacheHandler(IMemoryCache cache, CasterContext dbContext) +{ + protected async Task UpdateCache(Domain.Models.SystemRole systemRole) + { + var userIds = await dbContext.Users + .Where(x => x.RoleId == systemRole.Id) + .Select(x => x.Id) + .ToListAsync(); + + foreach (var userId in userIds) + { + cache.Remove(userId); + } + } +} + +public class SystemRoleUpdatedAuthCacheHandler(IMemoryCache memoryCache, CasterContext db) : SystemRoleBaseAuthCacheHandler(memoryCache, db), + INotificationHandler> +{ + public async Task Handle(EntityUpdated notification, CancellationToken cancellationToken) + { + if (notification.ModifiedProperties.Any(x => + x == nameof(SystemRole.Permissions) || + x == nameof(SystemRole.AllPermissions))) + { + await UpdateCache(notification.Entity); + } + } +} + +public class SystemRoleDeletedAuthCacheHandler(IMemoryCache memoryCache, CasterContext db) : SystemRoleBaseAuthCacheHandler(memoryCache, db), + INotificationHandler> +{ + public async Task Handle(EntityDeleted notification, CancellationToken cancellationToken) + { + await UpdateCache(notification.Entity); + } +} \ No newline at end of file diff --git a/src/Caster.Api/Features/SystemRoles/EventHandlers/SignalREventHandler.cs b/src/Caster.Api/Features/SystemRoles/EventHandlers/SignalREventHandler.cs new file mode 100644 index 0000000..922d2a2 --- /dev/null +++ b/src/Caster.Api/Features/SystemRoles/EventHandlers/SignalREventHandler.cs @@ -0,0 +1,61 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Caster.Api.Data; +using Caster.Api.Domain.Events; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Caster.Api.Hubs; +using Microsoft.AspNetCore.SignalR; + +namespace Caster.Api.Features.SystemRoles.EventHandlers; + +public class RoleCreatedSignalRHandler(CasterContext _db, IMapper _mapper, IHubContext _projectHub) : + RoleBaseSignalRHandler(_db, _mapper, _projectHub), + INotificationHandler> +{ + + public async Task Handle(EntityCreated notification, CancellationToken cancellationToken) + { + await base.Handle(notification.Entity, "RoleCreated", null, cancellationToken); + } +} + +public class RoleUpdatedSignalRHandler(CasterContext _db, IMapper _mapper, IHubContext _projectHub) : + RoleBaseSignalRHandler(_db, _mapper, _projectHub), + INotificationHandler> +{ + + public async Task Handle(EntityUpdated notification, CancellationToken cancellationToken) + { + await base.Handle(notification.Entity, "RoleUpdated", notification.ModifiedProperties, cancellationToken); + } +} + +public class RoleDeletedSignalRHandler(IHubContext _projectHub) : + INotificationHandler> +{ + + public async Task Handle(EntityDeleted notification, CancellationToken cancellationToken) + { + await _projectHub.Clients.Groups(nameof(HubGroups.RolesAdmin)).SendAsync("RoleDeleted", notification.Entity.Id, cancellationToken); + } +} + +public class RoleBaseSignalRHandler(CasterContext _db, IMapper _mapper, IHubContext _projectHub) +{ + protected async Task Handle(Domain.Models.SystemRole entity, string method, string[] modifiedProperties, CancellationToken cancellationToken) + { + var role = await _db.SystemRoles + .Where(r => r.Id == entity.Id) + .ProjectTo(_mapper.ConfigurationProvider) + .FirstOrDefaultAsync(); + + await _projectHub.Clients.Groups(nameof(HubGroups.RolesAdmin)).SendAsync(method, role, modifiedProperties, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Caster.Api/Features/SystemRoles/MappingProfile.cs b/src/Caster.Api/Features/SystemRoles/MappingProfile.cs new file mode 100644 index 0000000..b70f5bf --- /dev/null +++ b/src/Caster.Api/Features/SystemRoles/MappingProfile.cs @@ -0,0 +1,18 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +namespace Caster.Api.Features.SystemRoles +{ + using AutoMapper; + using System.Linq; + + public class MappingProfile : Profile + { + public MappingProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + } + } +} diff --git a/src/Caster.Api/Features/SystemRoles/Requests/Create.cs b/src/Caster.Api/Features/SystemRoles/Requests/Create.cs new file mode 100644 index 0000000..b1da1d3 --- /dev/null +++ b/src/Caster.Api/Features/SystemRoles/Requests/Create.cs @@ -0,0 +1,51 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using System.Runtime.Serialization; +using AutoMapper; +using Caster.Api.Data; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Features.SystemRoles +{ + public class Create + { + [DataContract(Name = "CreateSystemRoleCommand")] + public class Command : IRequest + { + [DataMember] + public Guid Id { get; set; } + + [DataMember] + public string Name { get; set; } + + [DataMember] + + public bool AllPermissions { get; set; } + + [DataMember] + public SystemPermission[] Permissions { get; set; } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageRoles], cancellationToken); + + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) + { + var systemRole = mapper.Map(request); + dbContext.SystemRoles.Add(systemRole); + await dbContext.SaveChangesAsync(); + return mapper.Map(systemRole); + } + } + } +} + diff --git a/src/Caster.Api/Features/SystemRoles/Requests/Delete.cs b/src/Caster.Api/Features/SystemRoles/Requests/Delete.cs new file mode 100644 index 0000000..5401d03 --- /dev/null +++ b/src/Caster.Api/Features/SystemRoles/Requests/Delete.cs @@ -0,0 +1,52 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using AutoMapper; +using Caster.Api.Data; +using System.Runtime.Serialization; +using System.Security.Claims; +using System.Security.Principal; +using Microsoft.AspNetCore.Authorization; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Infrastructure.Exceptions; +using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Features.SystemRoles +{ + public class Delete + { + [DataContract(Name = "DeleteSystemRoleCommand")] + public class Command : IRequest + { + public Guid Id { get; set; } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageRoles], cancellationToken); + + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) + { + var systemRole = await dbContext.SystemRoles.FindAsync([request.Id], cancellationToken); + + if (systemRole == null) + throw new EntityNotFoundException(); + + if (systemRole.Immutable) + throw new ConflictException("Immutable Role cannot be deleted."); + + dbContext.SystemRoles.Remove(systemRole); + await dbContext.SaveChangesAsync(cancellationToken); + } + } + } +} + diff --git a/src/Caster.Api/Features/SystemRoles/Requests/Edit.cs b/src/Caster.Api/Features/SystemRoles/Requests/Edit.cs new file mode 100644 index 0000000..2631e9b --- /dev/null +++ b/src/Caster.Api/Features/SystemRoles/Requests/Edit.cs @@ -0,0 +1,60 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Caster.Api.Data; +using AutoMapper; +using System.Runtime.Serialization; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Infrastructure.Exceptions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; +using Microsoft.AspNetCore.Identity; + +namespace Caster.Api.Features.SystemRoles +{ + public class Edit + { + [DataContract(Name = "EditSystemRoleCommand")] + public class Command : IRequest + { + [DataMember] + public Guid Id { get; set; } + + [DataMember] + public string Name { get; set; } + + [DataMember] + + public bool AllPermissions { get; set; } + + [DataMember] + public SystemPermission[] Permissions { get; set; } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageRoles], cancellationToken); + + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) + { + var systemRole = await dbContext.SystemRoles.FindAsync([request.Id], cancellationToken); + + if (systemRole == null) + throw new EntityNotFoundException(); + + if (systemRole.Immutable) + throw new ConflictException("Immutable Role cannot be changed."); + + mapper.Map(request, systemRole); + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(systemRole); + } + } + } +} + diff --git a/src/Caster.Api/Features/SystemRoles/Requests/Get.cs b/src/Caster.Api/Features/SystemRoles/Requests/Get.cs new file mode 100644 index 0000000..9b16bdb --- /dev/null +++ b/src/Caster.Api/Features/SystemRoles/Requests/Get.cs @@ -0,0 +1,50 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using AutoMapper.QueryableExtensions; +using System.Runtime.Serialization; +using Caster.Api.Data; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Infrastructure.Exceptions; +using Caster.Api.Features.Shared; + +namespace Caster.Api.Features.SystemRoles +{ + public class Get + { + [DataContract(Name = "GetSystemRoleQuery")] + public class Query : IRequest + { + /// + /// The Id of the SystemRole to retrieve + /// + [DataMember] + public Guid Id { get; set; } + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([Domain.Models.SystemPermission.ViewRoles], cancellationToken); + + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { + var systemRole = await dbContext.SystemRoles + .ProjectTo(mapper.ConfigurationProvider, dest => dest.Permissions) + .SingleOrDefaultAsync(e => e.Id == request.Id, cancellationToken); + + if (systemRole == null) + throw new EntityNotFoundException(); + + return systemRole; + } + } + } +} + diff --git a/src/Caster.Api/Features/SystemRoles/Requests/GetAll.cs b/src/Caster.Api/Features/SystemRoles/Requests/GetAll.cs new file mode 100644 index 0000000..97a85ca --- /dev/null +++ b/src/Caster.Api/Features/SystemRoles/Requests/GetAll.cs @@ -0,0 +1,41 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using AutoMapper.QueryableExtensions; +using System.Runtime.Serialization; +using Caster.Api.Data; +using Microsoft.AspNetCore.Authorization; +using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Infrastructure.Exceptions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Features.SystemRoles +{ + public class GetAll + { + [DataContract(Name = "GetSystemRolesQuery")] + public class Query : IRequest + { + } + + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewRoles, SystemPermission.ViewProjects], cancellationToken); + + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) + { + return await dbContext.SystemRoles + .ProjectTo(mapper.ConfigurationProvider) + .ToArrayAsync(cancellationToken); + } + } + } +} + diff --git a/src/Caster.Api/Features/SystemRoles/SystemRole.cs b/src/Caster.Api/Features/SystemRoles/SystemRole.cs new file mode 100644 index 0000000..fdf6ffa --- /dev/null +++ b/src/Caster.Api/Features/SystemRoles/SystemRole.cs @@ -0,0 +1,20 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System; + +namespace Caster.Api.Features.SystemRoles +{ + public class SystemRole + { + + public Guid Id { get; set; } + + public string Name { get; set; } + + public bool AllPermissions { get; set; } + public bool Immutable { get; set; } + public Domain.Models.SystemPermission[] Permissions { get; set; } + } +} + diff --git a/src/Caster.Api/Features/UserPermissions/UserPermissionsController.cs b/src/Caster.Api/Features/SystemRoles/SystemRolesController.cs similarity index 52% rename from src/Caster.Api/Features/UserPermissions/UserPermissionsController.cs rename to src/Caster.Api/Features/SystemRoles/SystemRolesController.cs index cfa8177..a65bac4 100644 --- a/src/Caster.Api/Features/UserPermissions/UserPermissionsController.cs +++ b/src/Caster.Api/Features/SystemRoles/SystemRolesController.cs @@ -10,41 +10,41 @@ using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; -namespace Caster.Api.Features.UserPermissions +namespace Caster.Api.Features.SystemRoles { - [Route("api/userPermissions")] + [Route("api/system-roles")] [ApiController] [Authorize] - public class UserPermissionsController : ControllerBase + public class SystemRolesController : ControllerBase { private readonly IMediator _mediator; - public UserPermissionsController(IMediator mediator) + public SystemRolesController(IMediator mediator) { _mediator = mediator; } - + /// - /// Get a single userPermission. + /// Get a single SystemRole. /// - /// ID of an userPermission. + /// ID of an SystemRole. /// [HttpGet("{id}")] - [ProducesResponseType(typeof(UserPermission), (int)HttpStatusCode.OK)] - [SwaggerOperation(OperationId = "GetUserPermission")] + [ProducesResponseType(typeof(SystemRole), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "GetSystemRole")] public async Task Get([FromRoute] Guid id) { var result = await _mediator.Send(new Get.Query { Id = id }); return Ok(result); } - + /// - /// Get all userPermissions. + /// Get all SystemRoles. /// /// [HttpGet()] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [SwaggerOperation(OperationId = "GetAllUserPermissions")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "GetAllSystemRoles")] public async Task GetAll() { var result = await _mediator.Send(new GetAll.Query()); @@ -52,13 +52,13 @@ public async Task GetAll() } /// - /// Create a new userPermission. + /// Create a new SystemRole. /// /// /// [HttpPost()] - [ProducesResponseType(typeof(UserPermission), (int)HttpStatusCode.Created)] - [SwaggerOperation(OperationId = "CreateUserPermission")] + [ProducesResponseType(typeof(SystemRole), (int)HttpStatusCode.Created)] + [SwaggerOperation(OperationId = "CreateSystemRole")] public async Task Create(Create.Command command) { var result = await _mediator.Send(command); @@ -66,50 +66,34 @@ public async Task Create(Create.Command command) } /// - /// Update a userPermission. + /// Update a SystemRole. /// - /// ID of an userPermission. + /// ID of an SystemRole. /// /// [HttpPut("{id}")] - [ProducesResponseType(typeof(UserPermission), (int)HttpStatusCode.OK)] - [SwaggerOperation(OperationId = "EditUserPermission")] + [ProducesResponseType(typeof(SystemRole), (int)HttpStatusCode.OK)] + [SwaggerOperation(OperationId = "EditSystemRole")] public async Task Edit([FromRoute] Guid id, Edit.Command command) { command.Id = id; var result = await _mediator.Send(command); return Ok(result); } - + /// - /// Delete a userPermission. + /// Delete a SystemRole. /// - /// ID of an userPermission. + /// ID of an SystemRole. /// [HttpDelete("{id}")] [ProducesResponseType((int)HttpStatusCode.NoContent)] - [SwaggerOperation(OperationId = "DeleteUserPermission")] + [SwaggerOperation(OperationId = "DeleteSystemRole")] public async Task Delete([FromRoute] Guid id) { await _mediator.Send(new Delete.Command { Id = id }); return NoContent(); } - - /// - /// Delete a userPermission by user and permission. - /// - /// ID of a user. - /// ID of a permission. - /// - [HttpDelete("/api/users/{userId}/permissions/{permissionId}")] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [SwaggerOperation(OperationId = "DeleteUserPermissionByIds")] - public async Task DeleteByIds([FromRoute] Guid userId, [FromRoute] Guid permissionId) - { - await _mediator.Send(new Delete.Command { UserId = userId, PermissionId = permissionId }); - return NoContent(); - } - } } diff --git a/src/Caster.Api/Features/Terraform/Requests/GetMaxParallelism.cs b/src/Caster.Api/Features/Terraform/Requests/GetMaxParallelism.cs index dd62074..cc416ec 100644 --- a/src/Caster.Api/Features/Terraform/Requests/GetMaxParallelism.cs +++ b/src/Caster.Api/Features/Terraform/Requests/GetMaxParallelism.cs @@ -5,19 +5,11 @@ using System.Threading.Tasks; using MediatR; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; -using Caster.Api.Domain.Services; -using System; -using Caster.Api.Data; -using AutoMapper; -using System.Text.Json.Serialization; -using System.Collections.Generic; using Caster.Api.Infrastructure.Options; using System.Linq; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Terraform { @@ -28,28 +20,24 @@ public class Query : IRequest { } - public class Handler : IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, TerraformOptions terraformOptions) : BaseHandler { - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly TerraformOptions _terraformOptions; - - public Handler( - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - TerraformOptions terraformOptions) + public async override Task Authorize(Query request, CancellationToken cancellationToken) { - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _terraformOptions = terraformOptions; + if (authorizationService.GetAuthorizedProjectIds().Any()) + { + return true; + } + else + { + return await authorizationService.Authorize([SystemPermission.ViewProjects], cancellationToken); + } } - public async Task Handle(Query request, CancellationToken cancellationToken) + public override Task HandleRequest(Query request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - return _terraformOptions.MaxParallelism; + return Task.FromResult(terraformOptions.MaxParallelism); } } } diff --git a/src/Caster.Api/Features/Terraform/Requests/GetVersions.cs b/src/Caster.Api/Features/Terraform/Requests/GetVersions.cs index 1819ae3..4ee413e 100644 --- a/src/Caster.Api/Features/Terraform/Requests/GetVersions.cs +++ b/src/Caster.Api/Features/Terraform/Requests/GetVersions.cs @@ -18,6 +18,8 @@ using System.Collections.Generic; using Caster.Api.Infrastructure.Options; using System.Linq; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Terraform { @@ -34,37 +36,32 @@ public class TerraformVersionsResult public string DefaultVersion { get; set; } } - public class Handler : IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + ITerraformService terraformService, + TerraformOptions terraformOptions) : BaseHandler { - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly ITerraformService _terraformService; - private readonly TerraformOptions _terraformOptions; - - public Handler( - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ITerraformService terraformService, - TerraformOptions terraformOptions) + public async override Task Authorize(Query request, CancellationToken cancellationToken) { - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _terraformService = terraformService; - _terraformOptions = terraformOptions; + if (authorizationService.GetAuthorizedProjectIds().Any()) + { + return true; + } + else + { + return await authorizationService.Authorize([SystemPermission.ViewProjects], cancellationToken); + } } - public async Task Handle(Query request, CancellationToken cancellationToken) + public override Task HandleRequest(Query request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var versions = _terraformService.GetVersions(); + var versions = terraformService.GetVersions(); - return new TerraformVersionsResult + return Task.FromResult(new TerraformVersionsResult { Versions = versions.ToArray(), - DefaultVersion = _terraformOptions.DefaultVersion - }; + DefaultVersion = terraformOptions.DefaultVersion + }); } } } diff --git a/src/Caster.Api/Features/UserPermissions/Requests/Create.cs b/src/Caster.Api/Features/UserPermissions/Requests/Create.cs deleted file mode 100644 index 4ed65b7..0000000 --- a/src/Caster.Api/Features/UserPermissions/Requests/Create.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System; -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using System.Runtime.Serialization; -using AutoMapper; -using Caster.Api.Data; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; -using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; - -namespace Caster.Api.Features.UserPermissions -{ - public class Create - { - [DataContract(Name="CreateUserPermissionCommand")] - public class Command : IRequest - { - [DataMember] - public Guid UserId { get; set; } - - [DataMember] - public Guid PermissionId { get; set; } - } - - public class Handler : IRequestHandler - { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var userPermission = _mapper.Map(request); - await _db.UserPermissions.AddAsync(userPermission); - await _db.SaveChangesAsync(); - return _mapper.Map(userPermission); - } - } - } -} - diff --git a/src/Caster.Api/Features/UserPermissions/Requests/Delete.cs b/src/Caster.Api/Features/UserPermissions/Requests/Delete.cs deleted file mode 100644 index 09e5fc0..0000000 --- a/src/Caster.Api/Features/UserPermissions/Requests/Delete.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using AutoMapper; -using Caster.Api.Data; -using System.Runtime.Serialization; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; -using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; - -namespace Caster.Api.Features.UserPermissions -{ - public class Delete - { - [DataContract(Name = "DeleteUserPermissionCommand")] - public class Command : IRequest - { - public Guid? Id { get; set; } - public Guid? UserId { get; set; } - public Guid? PermissionId { get; set; } - } - - public class Handler : IRequestHandler - { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - Caster.Api.Domain.Models.UserPermission entry; - - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - if (request.Id != null) - { - entry = _db.UserPermissions.FirstOrDefault(e => e.Id == request.Id); - } - else - { - entry = _db.UserPermissions.FirstOrDefault(e => e.UserId == request.UserId && e.PermissionId == request.PermissionId); - } - - if (entry == null) - throw new EntityNotFoundException(); - - _db.UserPermissions.Remove(entry); - await _db.SaveChangesAsync(cancellationToken); - } - } - } -} - diff --git a/src/Caster.Api/Features/UserPermissions/Requests/Edit.cs b/src/Caster.Api/Features/UserPermissions/Requests/Edit.cs deleted file mode 100644 index 644de0a..0000000 --- a/src/Caster.Api/Features/UserPermissions/Requests/Edit.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System; -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using Caster.Api.Data; -using AutoMapper; -using System.Runtime.Serialization; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; -using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; - -namespace Caster.Api.Features.UserPermissions -{ - public class Edit - { - [DataContract(Name="EditUserPermissionCommand")] - public class Command : IRequest - { - [DataMember] - public Guid Id { get; set; } - - [DataMember] - public Guid UserId { get; set; } - - [DataMember] - public Guid PermissionId { get; set; } - } - - public class Handler : IRequestHandler - { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var userPermission = await _db.UserPermissions.FindAsync(request.Id); - - if (userPermission == null) - throw new EntityNotFoundException(); - - _mapper.Map(request, userPermission); - await _db.SaveChangesAsync(); - return _mapper.Map(userPermission); - } - } - } -} - diff --git a/src/Caster.Api/Features/UserPermissions/Requests/Get.cs b/src/Caster.Api/Features/UserPermissions/Requests/Get.cs deleted file mode 100644 index d011c39..0000000 --- a/src/Caster.Api/Features/UserPermissions/Requests/Get.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System; -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using AutoMapper; -using Microsoft.EntityFrameworkCore; -using AutoMapper.QueryableExtensions; -using System.Runtime.Serialization; -using Caster.Api.Data; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; -using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; - -namespace Caster.Api.Features.UserPermissions -{ - public class Get - { - [DataContract(Name="GetUserPermissionQuery")] - public class Query : IRequest - { - /// - /// The Id of the UserPermission to retrieve - /// - [DataMember] - public Guid Id { get; set; } - } - - public class Handler : IRequestHandler - { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var userPermission = await _db.UserPermissions - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(e => e.Id == request.Id); - - if (userPermission == null) - throw new EntityNotFoundException(); - - return userPermission; - } - } - } -} - diff --git a/src/Caster.Api/Features/UserPermissions/Requests/GetAll.cs b/src/Caster.Api/Features/UserPermissions/Requests/GetAll.cs deleted file mode 100644 index 7315e18..0000000 --- a/src/Caster.Api/Features/UserPermissions/Requests/GetAll.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using AutoMapper; -using Microsoft.EntityFrameworkCore; -using AutoMapper.QueryableExtensions; -using System.Runtime.Serialization; -using Caster.Api.Data; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; -using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; - -namespace Caster.Api.Features.UserPermissions -{ - public class GetAll - { - [DataContract(Name="GetUserPermissionsQuery")] - public class Query : IRequest - { - } - - public class Handler : IRequestHandler - { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - return await _db.UserPermissions - .ProjectTo(_mapper.ConfigurationProvider) - .ToArrayAsync(); - } - } - } -} - diff --git a/src/Caster.Api/Features/Users/EventHandlers/AuthCacheEventHandler.cs b/src/Caster.Api/Features/Users/EventHandlers/AuthCacheEventHandler.cs new file mode 100644 index 0000000..cb43cfc --- /dev/null +++ b/src/Caster.Api/Features/Users/EventHandlers/AuthCacheEventHandler.cs @@ -0,0 +1,35 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Caster.Api.Domain.Events; +using MediatR; +using Microsoft.Extensions.Caching.Memory; + +namespace Caster.Api.Features.Projects.EventHandlers; + +public class UserUpdatedAuthCacheHandler(IMemoryCache cache) : + INotificationHandler> +{ + public Task Handle(EntityUpdated notification, CancellationToken cancellationToken) + { + if (notification.ModifiedProperties.Any(x => x == nameof(Domain.Models.User.RoleId))) + { + cache.Remove(notification.Entity.Id); + } + + return Task.CompletedTask; + } +} + +public class UserDeletedAuthCacheHandler(IMemoryCache cache) : + INotificationHandler> +{ + public Task Handle(EntityDeleted notification, CancellationToken cancellationToken) + { + cache.Remove(notification.Entity.Id); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Caster.Api/Features/Users/MappingProfile.cs b/src/Caster.Api/Features/Users/MappingProfile.cs index d22ec27..683dd5c 100644 --- a/src/Caster.Api/Features/Users/MappingProfile.cs +++ b/src/Caster.Api/Features/Users/MappingProfile.cs @@ -10,9 +10,7 @@ public class MappingProfile : Profile { public MappingProfile() { - CreateMap() - .ForMember(m => m.Permissions, opt => opt.MapFrom(x => x.UserPermissions.Select(y => y.Permission))) - .ForMember(m => m.Permissions, opt => opt.ExplicitExpansion()); + CreateMap(); CreateMap(); CreateMap(); } diff --git a/src/Caster.Api/Features/Users/Requests/Create.cs b/src/Caster.Api/Features/Users/Requests/Create.cs index 99c9709..4f0f579 100644 --- a/src/Caster.Api/Features/Users/Requests/Create.cs +++ b/src/Caster.Api/Features/Users/Requests/Create.cs @@ -8,18 +8,15 @@ using System.Runtime.Serialization; using AutoMapper; using Caster.Api.Data; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Users { public class Create { - [DataContract(Name="CreateUserCommand")] + [DataContract(Name = "CreateUserCommand")] public class Command : IRequest { [DataMember] @@ -27,36 +24,26 @@ public class Command : IRequest [DataMember] public string Name { get; set; } + + [DataMember] + + public bool AllPermissions { get; set; } + + [DataMember] + public string RoleId { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageUsers], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var user = _mapper.Map(request); - await _db.Users.AddAsync(user); - await _db.SaveChangesAsync(); - return _mapper.Map(user); + var user = mapper.Map(request); + dbContext.Users.Add(user); + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(user); } } } diff --git a/src/Caster.Api/Features/Users/Requests/Delete.cs b/src/Caster.Api/Features/Users/Requests/Delete.cs index 98d65a7..89dcd2d 100644 --- a/src/Caster.Api/Features/Users/Requests/Delete.cs +++ b/src/Caster.Api/Features/Users/Requests/Delete.cs @@ -2,19 +2,15 @@ // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; using MediatR; -using AutoMapper; using Caster.Api.Data; using System.Runtime.Serialization; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Domain.Models; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Users { @@ -26,37 +22,20 @@ public class Command : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageUsers], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var entry = _db.Users.FirstOrDefault(e => e.Id == request.Id); + var user = await dbContext.Users.FindAsync([request.Id], cancellationToken); - if (entry == null) + if (user == null) throw new EntityNotFoundException(); - _db.Users.Remove(entry); - await _db.SaveChangesAsync(cancellationToken); + dbContext.Users.Remove(user); + await dbContext.SaveChangesAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Users/Requests/Edit.cs b/src/Caster.Api/Features/Users/Requests/Edit.cs index c4945d6..1c8b413 100644 --- a/src/Caster.Api/Features/Users/Requests/Edit.cs +++ b/src/Caster.Api/Features/Users/Requests/Edit.cs @@ -8,18 +8,16 @@ using Caster.Api.Data; using AutoMapper; using System.Runtime.Serialization; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Domain.Models; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Users { public class Edit { - [DataContract(Name="EditUserCommand")] + [DataContract(Name = "EditUserCommand")] public class Command : IRequest { [DataMember] @@ -27,40 +25,26 @@ public class Command : IRequest [DataMember] public string Name { get; set; } + + [DataMember] + public string RoleId { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageUsers], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var user = await _db.Users.FindAsync(request.Id); + var user = await dbContext.Users.FindAsync([request.Id], cancellationToken); if (user == null) throw new EntityNotFoundException(); - _mapper.Map(request, user); - await _db.SaveChangesAsync(); - return _mapper.Map(user); + mapper.Map(request, user); + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(user); } } } diff --git a/src/Caster.Api/Features/Users/Requests/Get.cs b/src/Caster.Api/Features/Users/Requests/Get.cs index 3d3c6bb..a1010bc 100644 --- a/src/Caster.Api/Features/Users/Requests/Get.cs +++ b/src/Caster.Api/Features/Users/Requests/Get.cs @@ -16,12 +16,14 @@ using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Infrastructure.Identity; +using Caster.Api.Domain.Models; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Users { public class Get { - [DataContract(Name="GetUserQuery")] + [DataContract(Name = "GetUserQuery")] public class Query : IRequest { /// @@ -31,33 +33,16 @@ public class Query : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewUsers], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var user = await _db.Users - .ProjectTo(_mapper.ConfigurationProvider, dest => dest.Permissions) - .SingleOrDefaultAsync(e => e.Id == request.Id); + var user = await dbContext.Users + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(e => e.Id == request.Id, cancellationToken); if (user == null) throw new EntityNotFoundException(); diff --git a/src/Caster.Api/Features/Users/Requests/GetAll.cs b/src/Caster.Api/Features/Users/Requests/GetAll.cs index 467f499..eac5e71 100644 --- a/src/Caster.Api/Features/Users/Requests/GetAll.cs +++ b/src/Caster.Api/Features/Users/Requests/GetAll.cs @@ -15,43 +15,38 @@ using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; +using System.Linq; namespace Caster.Api.Features.Users { public class GetAll { - [DataContract(Name="GetUsersQuery")] + [DataContract(Name = "GetUsersQuery")] public class Query : IRequest { } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task Authorize(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); + if (await authorizationService.Authorize([SystemPermission.ViewUsers, SystemPermission.ViewProjects], cancellationToken)) + { + return true; + } + + return authorizationService. + GetProjectPermissions() + .Any(x => x.Permissions.Contains(ProjectPermission.ManageProject)); } - public async Task Handle(Query request, CancellationToken cancellationToken) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - return await _db.Users - .ProjectTo(_mapper.ConfigurationProvider, dest => dest.Permissions) - .ToArrayAsync(); + return await dbContext.Users + .ProjectTo(mapper.ConfigurationProvider) + .ToArrayAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Users/Requests/GetByPermission.cs b/src/Caster.Api/Features/Users/Requests/GetByPermission.cs deleted file mode 100644 index a1072b7..0000000 --- a/src/Caster.Api/Features/Users/Requests/GetByPermission.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2021 Carnegie Mellon University. All Rights Reserved. -// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using AutoMapper; -using Microsoft.EntityFrameworkCore; -using AutoMapper.QueryableExtensions; -using System.Runtime.Serialization; -using Caster.Api.Data; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; -using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; - -namespace Caster.Api.Features.Users -{ - public class GetUsersByPermission - { - [DataContract(Name="GetUsersByPermissionQuery")] - public class Query : IRequest - { - /// - /// The Id of the Permission to query by - /// - [DataMember] - public Guid PermissionId { get; set; } - } - - public class Handler : IRequestHandler - { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - - var users = await _db.Users.Where(u => u.UserPermissions.Any(up => up.PermissionId == request.PermissionId)) - .ProjectTo(_mapper.ConfigurationProvider, dest => dest.Permissions) - .ToArrayAsync(); - - return users; - } - } - } -} - diff --git a/src/Caster.Api/Features/Users/User.cs b/src/Caster.Api/Features/Users/User.cs index e3360a7..2b18555 100644 --- a/src/Caster.Api/Features/Users/User.cs +++ b/src/Caster.Api/Features/Users/User.cs @@ -2,7 +2,6 @@ // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. using System; -using Caster.Api.Features.Permissions; namespace Caster.Api.Features.Users { @@ -12,7 +11,7 @@ public class User public Guid Id { get; set; } public string Name { get; set; } - public Permission[] Permissions { get; set; } + public string RoleId { get; set; } } } diff --git a/src/Caster.Api/Features/Users/UsersController.cs b/src/Caster.Api/Features/Users/UsersController.cs index a9a0cd6..40b22c2 100644 --- a/src/Caster.Api/Features/Users/UsersController.cs +++ b/src/Caster.Api/Features/Users/UsersController.cs @@ -19,11 +19,11 @@ public class UsersController : ControllerBase { private readonly IMediator _mediator; - public UsersController(IMediator mediator) + public UsersController(IMediator mediator) { _mediator = mediator; } - + /// /// Get a single user. /// @@ -37,7 +37,7 @@ public async Task Get([FromRoute] Guid id) var result = await _mediator.Send(new Get.Query { Id = id }); return Ok(result); } - + /// /// Get all users. /// @@ -51,20 +51,6 @@ public async Task GetAll() return Ok(result); } - /// - /// Get users with the specified permission. - /// - /// ID of a permission. - /// - [HttpGet("permissions/{permissionId}/users")] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [SwaggerOperation(OperationId = "GetUsersWithPermission")] - public async Task GetUsersWithPermission([FromRoute] Guid permissionId) - { - var result = await _mediator.Send(new GetUsersByPermission.Query { PermissionId = permissionId }); - return Ok(result); - } - /// /// Create a new user. /// @@ -94,7 +80,7 @@ public async Task Edit([FromRoute] Guid id, Edit.Command command) var result = await _mediator.Send(command); return Ok(result); } - + /// /// Delete a user. /// diff --git a/src/Caster.Api/Features/Variables/Requests/Create.cs b/src/Caster.Api/Features/Variables/Requests/Create.cs index a28ebfd..b9f713c 100644 --- a/src/Caster.Api/Features/Variables/Requests/Create.cs +++ b/src/Caster.Api/Features/Variables/Requests/Create.cs @@ -6,14 +6,14 @@ using System.Threading.Tasks; using MediatR; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Features.Shared; using FluentValidation; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; using Caster.Api.Domain.Models; +using AutoMapper; +using Caster.Api.Data; namespace Caster.Api.Features.Variables; @@ -36,21 +36,19 @@ public Validator(IValidationService validationService) } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.DesignId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); + var variable = mapper.Map(request); - var variable = _mapper.Map(request); + dbContext.Variables.Add(variable); + await dbContext.SaveChangesAsync(cancellationToken); - _db.Variables.Add(variable); - await _db.SaveChangesAsync(cancellationToken); - - return _mapper.Map(variable); + return mapper.Map(variable); } } } diff --git a/src/Caster.Api/Features/Variables/Requests/Delete.cs b/src/Caster.Api/Features/Variables/Requests/Delete.cs index 8c6c188..da8fb39 100644 --- a/src/Caster.Api/Features/Variables/Requests/Delete.cs +++ b/src/Caster.Api/Features/Variables/Requests/Delete.cs @@ -7,10 +7,11 @@ using MediatR; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Features.Shared; using System.Text.Json.Serialization; +using Caster.Api.Domain.Models; +using Caster.Api.Data; namespace Caster.Api.Features.Variables; @@ -23,22 +24,20 @@ public record Command : IRequest public Guid Id { get; set; } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var variable = await _db.Variables.FindAsync(request.Id); + var variable = await dbContext.Variables.FindAsync([request.Id], cancellationToken); if (variable == null) throw new EntityNotFoundException(); - _db.Variables.Remove(variable); - await _db.SaveChangesAsync(cancellationToken); + dbContext.Variables.Remove(variable); + await dbContext.SaveChangesAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Variables/Requests/Edit.cs b/src/Caster.Api/Features/Variables/Requests/Edit.cs index fbb8fa2..2d5a0b4 100644 --- a/src/Caster.Api/Features/Variables/Requests/Edit.cs +++ b/src/Caster.Api/Features/Variables/Requests/Edit.cs @@ -7,7 +7,6 @@ using MediatR; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Features.Shared; using FluentValidation; @@ -15,6 +14,8 @@ using Microsoft.EntityFrameworkCore; using System.Text.Json.Serialization; using Caster.Api.Domain.Models; +using AutoMapper; +using Caster.Api.Data; namespace Caster.Api.Features.Variables; @@ -31,27 +32,24 @@ public record Command : IRequest public string DefaultValue { get; init; } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var variable = await _db.Variables + var variable = await dbContext.Variables .Where(x => x.Id == request.Id) .SingleOrDefaultAsync(cancellationToken); if (variable == null) throw new EntityNotFoundException(); - _mapper.Map(request, variable); - - await _db.SaveChangesAsync(cancellationToken); + mapper.Map(request, variable); + await dbContext.SaveChangesAsync(cancellationToken); - return _mapper.Map(variable); + return mapper.Map(variable); } } } diff --git a/src/Caster.Api/Features/Variables/Requests/Get.cs b/src/Caster.Api/Features/Variables/Requests/Get.cs index 1c3ecc1..242318a 100644 --- a/src/Caster.Api/Features/Variables/Requests/Get.cs +++ b/src/Caster.Api/Features/Variables/Requests/Get.cs @@ -15,6 +15,9 @@ using Caster.Api.Features.Shared; using FluentValidation; using AutoMapper.QueryableExtensions; +using Caster.Api.Domain.Models; +using AutoMapper; +using Caster.Api.Data; namespace Caster.Api.Features.Variables; @@ -27,18 +30,16 @@ public record Query : IRequest public Guid Id { get; set; } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public async Task Handle(Query request, CancellationToken cancellationToken) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var variable = await _db.Variables + var variable = await dbContext.Variables .Where(x => x.Id == request.Id) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .SingleOrDefaultAsync(cancellationToken); if (variable == null) diff --git a/src/Caster.Api/Features/Variables/Requests/GetAll.cs b/src/Caster.Api/Features/Variables/Requests/GetAll.cs index 1eb9797..4aed564 100644 --- a/src/Caster.Api/Features/Variables/Requests/GetAll.cs +++ b/src/Caster.Api/Features/Variables/Requests/GetAll.cs @@ -7,16 +7,16 @@ using MediatR; using Microsoft.EntityFrameworkCore; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using System.Text.Json.Serialization; using System.Linq; using Caster.Api.Features.Shared; using FluentValidation; using AutoMapper.QueryableExtensions; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using AutoMapper; +using Caster.Api.Data; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Variables; @@ -36,16 +36,14 @@ public Validator(IValidationService validationService) } } - public class Handler : BaseHandler, IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - public Handler(IDependencyAggregate aggregate) : base(aggregate) { } + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.DesignId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public async Task Handle(Query request, CancellationToken cancellationToken) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var variablesQuery = _db.Variables.AsQueryable(); + var variablesQuery = dbContext.Variables.AsQueryable(); if (request.DesignId.HasValue) { @@ -53,7 +51,7 @@ public async Task Handle(Query request, CancellationToken cancellati } var variables = await variablesQuery - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .ToArrayAsync(cancellationToken); return variables; diff --git a/src/Caster.Api/Features/Vlan/Requests/Partitions/AssignPartition.cs b/src/Caster.Api/Features/Vlan/Requests/Partitions/AssignPartition.cs index 2a0dec6..8b3e090 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Partitions/AssignPartition.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Partitions/AssignPartition.cs @@ -1,25 +1,24 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + using System; using System.Linq; using System.Runtime.Serialization; -using System.Security.Claims; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; -using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; -using Microsoft.EntityFrameworkCore; -using System.Text.Json.Serialization; -using Caster.Api.Features.Projects; -using FluentValidation; +using Caster.Api.Domain.Models; +using Caster.Api.Features.Shared; using Caster.Api.Features.Shared.Services; +using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Extensions; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Project = Caster.Api.Features.Projects.Project; namespace Caster.Api.Features.Vlan { @@ -35,7 +34,7 @@ public class Command : IRequest public Guid PartitionId { get; set; } /// - /// The Id of the Project + /// The Id of the Project /// [DataMember] public Guid ProjectId { get; set; } @@ -50,39 +49,22 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command command, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command command, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var project = await _db.Projects + var project = await dbContext.Projects .Where(x => x.Id == command.ProjectId) .FirstOrDefaultAsync(cancellationToken); project.PartitionId = command.PartitionId; - await _db.SaveChangesAsync(); + await dbContext.SaveChangesAsync(cancellationToken); - return _mapper.Map(project); + return mapper.Map(project); } } } diff --git a/src/Caster.Api/Features/Vlan/Requests/Partitions/CreatePartition.cs b/src/Caster.Api/Features/Vlan/Requests/Partitions/CreatePartition.cs index b23000d..cae2274 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Partitions/CreatePartition.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Partitions/CreatePartition.cs @@ -4,24 +4,20 @@ using System; using System.Linq; using System.Runtime.Serialization; -using System.Security.Claims; -using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Microsoft.EntityFrameworkCore; -using AutoMapper.QueryableExtensions; -using System.Collections.Generic; using FluentValidation; using System.Text.Json.Serialization; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan { @@ -59,37 +55,18 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly IIdentityResolver _identityResolver; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command command, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _identityResolver = identityResolver; - } - - public async Task Handle(Command command, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var vlans = await _db.Vlans + var vlans = await dbContext.Vlans .Where(x => x.PoolId == command.PoolId && !x.InUse && !x.Reserved) .OrderBy(x => x.VlanId) .Take(command.Vlans) - .ToArrayAsync(); + .ToArrayAsync(cancellationToken); // Verify there are enough available vlans in this pool if (vlans.Length < command.Vlans) @@ -99,12 +76,12 @@ public async Task Handle(Command command, CancellationToken cancellat } // Create partition - var partition = _mapper.Map(command); + var partition = mapper.Map(command); partition.Vlans = vlans; - _db.Partitions.Add(partition); - await _db.SaveChangesAsync(); + dbContext.Partitions.Add(partition); + await dbContext.SaveChangesAsync(cancellationToken); - return _mapper.Map(partition); + return mapper.Map(partition); } } } diff --git a/src/Caster.Api/Features/Vlan/Requests/Partitions/DeletePartition.cs b/src/Caster.Api/Features/Vlan/Requests/Partitions/DeletePartition.cs index aa6e2d1..005af99 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Partitions/DeletePartition.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Partitions/DeletePartition.cs @@ -3,22 +3,20 @@ using System; using System.Runtime.Serialization; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using System.Linq; using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; using FluentValidation; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan { @@ -39,36 +37,19 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var partition = await _db.Partitions + var partition = await dbContext.Partitions .Where(x => x.Id == request.Id) .FirstOrDefaultAsync(cancellationToken); - _db.Partitions.Remove(partition); - await _db.SaveChangesAsync(cancellationToken); + dbContext.Partitions.Remove(partition); + await dbContext.SaveChangesAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Vlan/Requests/Partitions/EditPartition.cs b/src/Caster.Api/Features/Vlan/Requests/Partitions/EditPartition.cs index 82473d1..71a18e5 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Partitions/EditPartition.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Partitions/EditPartition.cs @@ -4,24 +4,19 @@ using System; using System.Linq; using System.Runtime.Serialization; -using System.Security.Claims; -using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Microsoft.EntityFrameworkCore; -using AutoMapper.QueryableExtensions; -using System.Collections.Generic; using FluentValidation; using System.Text.Json.Serialization; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan { @@ -48,38 +43,21 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command command, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command command, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var partition = await _db.Partitions + var partition = await dbContext.Partitions .Where(x => x.Id == command.Id) .FirstOrDefaultAsync(cancellationToken); - _mapper.Map(command, partition); - await _db.SaveChangesAsync(); + mapper.Map(command, partition); + await dbContext.SaveChangesAsync(); - return _mapper.Map(partition); + return mapper.Map(partition); } } } diff --git a/src/Caster.Api/Features/Vlan/Requests/Partitions/GetPartition.cs b/src/Caster.Api/Features/Vlan/Requests/Partitions/GetPartition.cs index 03e7312..6a86956 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Partitions/GetPartition.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Partitions/GetPartition.cs @@ -2,20 +2,19 @@ // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. using System.Runtime.Serialization; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Microsoft.EntityFrameworkCore; using AutoMapper.QueryableExtensions; using System; using System.Linq; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan { @@ -27,33 +26,16 @@ public class Query : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query query, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query query, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var partition = await _db.Partitions + var partition = await dbContext.Partitions .Where(x => x.Id == query.Id) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .FirstOrDefaultAsync(cancellationToken); if (partition == null) diff --git a/src/Caster.Api/Features/Vlan/Requests/Partitions/GetPartitions.cs b/src/Caster.Api/Features/Vlan/Requests/Partitions/GetPartitions.cs index 2f11723..df284c0 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Partitions/GetPartitions.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Partitions/GetPartitions.cs @@ -2,16 +2,12 @@ // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. using System.Runtime.Serialization; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Microsoft.EntityFrameworkCore; using AutoMapper.QueryableExtensions; using System; @@ -19,6 +15,8 @@ using FluentValidation; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan { @@ -39,31 +37,14 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query query, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query query, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var partitionQuery = _db.Partitions.AsQueryable(); + var partitionQuery = dbContext.Partitions.AsQueryable(); if (query.PoolId.HasValue) { @@ -71,7 +52,7 @@ public async Task Handle(Query query, CancellationToken cancellatio } var partitions = await partitionQuery - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .ToArrayAsync(cancellationToken); return partitions; diff --git a/src/Caster.Api/Features/Vlan/Requests/Partitions/PartialEditPartition.cs b/src/Caster.Api/Features/Vlan/Requests/Partitions/PartialEditPartition.cs index 661ea5c..92a75fc 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Partitions/PartialEditPartition.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Partitions/PartialEditPartition.cs @@ -4,24 +4,19 @@ using System; using System.Linq; using System.Runtime.Serialization; -using System.Security.Claims; -using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Microsoft.EntityFrameworkCore; -using AutoMapper.QueryableExtensions; -using System.Collections.Generic; using FluentValidation; using System.Text.Json.Serialization; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan { @@ -48,38 +43,21 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command command, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command command, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var partition = await _db.Partitions + var partition = await dbContext.Partitions .Where(x => x.Id == command.Id) .FirstOrDefaultAsync(cancellationToken); - _mapper.Map(command, partition); - await _db.SaveChangesAsync(); + mapper.Map(command, partition); + await dbContext.SaveChangesAsync(); - return _mapper.Map(partition); + return mapper.Map(partition); } } } diff --git a/src/Caster.Api/Features/Vlan/Requests/Partitions/SetDefaultPartition.cs b/src/Caster.Api/Features/Vlan/Requests/Partitions/SetDefaultPartition.cs index 1fcde6b..c2548b1 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Partitions/SetDefaultPartition.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Partitions/SetDefaultPartition.cs @@ -22,6 +22,8 @@ using System.Text.Json.Serialization; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan { @@ -42,38 +44,21 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command command, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command command, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - // Ensure that only one partition/pool can be Default - using var transaction = _db.Database.BeginTransaction(System.Data.IsolationLevel.Serializable); + using var transaction = dbContext.Database.BeginTransaction(System.Data.IsolationLevel.Serializable); - var currentDefaultPool = await _db.Pools + var currentDefaultPool = await dbContext.Pools .Where(x => x.IsDefault) .FirstOrDefaultAsync(cancellationToken); - var currentDefaultPartition = await _db.Partitions + var currentDefaultPartition = await dbContext.Partitions .Where(x => x.IsDefault) .FirstOrDefaultAsync(cancellationToken); @@ -85,7 +70,7 @@ public async Task Handle(Command command, CancellationToken cancellationToken) if (command.Id.HasValue) { - var partition = await _db.Partitions + var partition = await dbContext.Partitions .Where(x => x.Id == command.Id) .Include(x => x.Pool) .FirstOrDefaultAsync(cancellationToken); @@ -94,7 +79,7 @@ public async Task Handle(Command command, CancellationToken cancellationToken) partition.Pool.IsDefault = true; } - await _db.SaveChangesAsync(cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); } } diff --git a/src/Caster.Api/Features/Vlan/Requests/Partitions/UnassignPartition.cs b/src/Caster.Api/Features/Vlan/Requests/Partitions/UnassignPartition.cs index a9e6615..3db9db9 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Partitions/UnassignPartition.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Partitions/UnassignPartition.cs @@ -4,21 +4,19 @@ using System; using System.Linq; using System.Runtime.Serialization; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Microsoft.EntityFrameworkCore; -using Caster.Api.Features.Projects; using FluentValidation; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; +using Project = Caster.Api.Features.Projects.Project; namespace Caster.Api.Features.Vlan { @@ -42,38 +40,21 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command command, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command command, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var project = await _db.Projects + var project = await dbContext.Projects .Where(x => x.Id == command.ProjectId) .FirstOrDefaultAsync(cancellationToken); project.PartitionId = null; - await _db.SaveChangesAsync(); + await dbContext.SaveChangesAsync(); - return _mapper.Map(project); + return mapper.Map(project); } } } diff --git a/src/Caster.Api/Features/Vlan/Requests/Pools/CreatePool.cs b/src/Caster.Api/Features/Vlan/Requests/Pools/CreatePool.cs index 375dc51..7f073bb 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Pools/CreatePool.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Pools/CreatePool.cs @@ -3,21 +3,18 @@ using System; using System.Runtime.Serialization; -using System.Security.Claims; -using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using System.Linq; using System.ComponentModel; using EFCore.BulkExtensions; using System.Collections.Generic; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan { @@ -37,36 +34,19 @@ public class Command : IRequest /// [DataMember] [DefaultValue(new int[] { 0, 1, 4095 })] - public int[] ReservedVlanIds { get; set; } = new int[] { 0, 1, 4095 }; + public int[] ReservedVlanIds { get; set; } = [0, 1, 4095]; } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var pool = _mapper.Map(request); - _db.Pools.Add(pool); - await _db.SaveChangesAsync(cancellationToken); + var pool = mapper.Map(request); + dbContext.Pools.Add(pool); + await dbContext.SaveChangesAsync(cancellationToken); var vlans = new List(); @@ -80,10 +60,10 @@ public async Task Handle(Command request, CancellationToken cancellationTo }); } - await _db.BulkInsertAsync(vlans, + await dbContext.BulkInsertAsync(vlans, new BulkConfig { PropertiesToExclude = new List { nameof(Pool.Id) } }); // workaround until id properties in pgsql are fixed - return _mapper.Map(pool); + return mapper.Map(pool); } } } diff --git a/src/Caster.Api/Features/Vlan/Requests/Pools/DeletePool.cs b/src/Caster.Api/Features/Vlan/Requests/Pools/DeletePool.cs index b37c410..45f06e0 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Pools/DeletePool.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Pools/DeletePool.cs @@ -3,23 +3,20 @@ using System; using System.Runtime.Serialization; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using MediatR; -using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using System.Linq; -using System.Collections.Generic; using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; using FluentValidation; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan { @@ -46,37 +43,20 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var pool = await _db.Pools + var pool = await dbContext.Pools .Where(x => x.Id == request.Id) .FirstOrDefaultAsync(cancellationToken); if (!request.Force) { - var inUse = await _db.Vlans + var inUse = await dbContext.Vlans .Where(x => x.PoolId == pool.Id && x.InUse) .AnyAsync(cancellationToken); @@ -86,8 +66,8 @@ public async Task Handle(Command request, CancellationToken cancellationToken) } } - _db.Pools.Remove(pool); - await _db.SaveChangesAsync(cancellationToken); + dbContext.Pools.Remove(pool); + await dbContext.SaveChangesAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Vlan/Requests/Pools/EditPool.cs b/src/Caster.Api/Features/Vlan/Requests/Pools/EditPool.cs index 58e5d79..20f2ecb 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Pools/EditPool.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Pools/EditPool.cs @@ -4,21 +4,19 @@ using System; using System.Linq; using System.Runtime.Serialization; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Microsoft.EntityFrameworkCore; using FluentValidation; using System.Text.Json.Serialization; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan { @@ -45,38 +43,21 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command command, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command command, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var pool = await _db.Pools + var pool = await dbContext.Pools .Where(x => x.Id == command.Id) .FirstOrDefaultAsync(cancellationToken); - _mapper.Map(command, pool); - await _db.SaveChangesAsync(); + mapper.Map(command, pool); + await dbContext.SaveChangesAsync(); - return _mapper.Map(pool); + return mapper.Map(pool); } } } diff --git a/src/Caster.Api/Features/Vlan/Requests/Pools/GetPool.cs b/src/Caster.Api/Features/Vlan/Requests/Pools/GetPool.cs index c20d925..d8a312e 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Pools/GetPool.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Pools/GetPool.cs @@ -4,19 +4,17 @@ using System; using System.Linq; using System.Runtime.Serialization; -using System.Security.Claims; -using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Microsoft.EntityFrameworkCore; using AutoMapper.QueryableExtensions; +using Caster.Api.Domain.Models; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Vlan { @@ -32,34 +30,17 @@ public class Query : IRequest public Guid PoolId { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var pool = await _db.Pools + var pool = await dbContext.Pools .Where(p => p.Id == request.PoolId) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(cancellationToken); if (pool == null) throw new EntityNotFoundException(); diff --git a/src/Caster.Api/Features/Vlan/Requests/Pools/GetPools.cs b/src/Caster.Api/Features/Vlan/Requests/Pools/GetPools.cs index 8daabce..a8a9fa4 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Pools/GetPools.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Pools/GetPools.cs @@ -2,18 +2,16 @@ // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. using System.Runtime.Serialization; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Microsoft.EntityFrameworkCore; using AutoMapper.QueryableExtensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan { @@ -24,33 +22,16 @@ public class Query : IRequest { } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query poolRequest, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query poolRequest, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var pools = await _db.Pools - .ProjectTo(_mapper.ConfigurationProvider) - .ToArrayAsync(); + var pools = await dbContext.Pools + .ProjectTo(mapper.ConfigurationProvider) + .ToArrayAsync(cancellationToken); return pools; } diff --git a/src/Caster.Api/Features/Vlan/Requests/Pools/PartialEditPool.cs b/src/Caster.Api/Features/Vlan/Requests/Pools/PartialEditPool.cs index d1c5a53..f963bfb 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Pools/PartialEditPool.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Pools/PartialEditPool.cs @@ -4,21 +4,19 @@ using System; using System.Linq; using System.Runtime.Serialization; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Microsoft.EntityFrameworkCore; using FluentValidation; using System.Text.Json.Serialization; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Domain.Models; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Vlan { @@ -45,38 +43,21 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command command, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command command, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var pool = await _db.Pools + var pool = await dbContext.Pools .Where(x => x.Id == command.Id) .FirstOrDefaultAsync(cancellationToken); - _mapper.Map(command, pool); - await _db.SaveChangesAsync(); + mapper.Map(command, pool); + await dbContext.SaveChangesAsync(cancellationToken); - return _mapper.Map(pool); + return mapper.Map(pool); } } } diff --git a/src/Caster.Api/Features/Vlan/Requests/Vlans/AcquireVlan.cs b/src/Caster.Api/Features/Vlan/Requests/Vlans/AcquireVlan.cs index be47125..c26e035 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Vlans/AcquireVlan.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Vlans/AcquireVlan.cs @@ -4,22 +4,20 @@ using System; using System.Linq; using System.Runtime.Serialization; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Caster.Api.Domain.Services; -using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; using FluentValidation; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan; @@ -54,46 +52,30 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + ILockService lockService) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly ILockService _lockService; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _lockService = lockService; - } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public async Task Handle(Command command, CancellationToken cancellationToken) + public override async Task HandleRequest(Command command, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - Guid? partitionId = null; // find partition if (command.PartitionId.HasValue) { - partitionId = await _db.Partitions + partitionId = await dbContext.Partitions .Where(x => x.Id == command.PartitionId.Value) .Select(x => x.Id) .FirstOrDefaultAsync(cancellationToken); } else if (command.ProjectId.HasValue) { - partitionId = await _db.Projects + partitionId = await dbContext.Projects .Where(x => x.Id == command.ProjectId.Value) .Select(x => x.PartitionId) .FirstOrDefaultAsync(cancellationToken); @@ -101,7 +83,7 @@ public async Task Handle(Command command, CancellationToken cancellationTo else { // Use default partition if one exists - partitionId = await _db.Partitions + partitionId = await dbContext.Partitions .Where(x => x.IsDefault) .Select(x => x.Id) .FirstOrDefaultAsync(cancellationToken); @@ -112,12 +94,12 @@ public async Task Handle(Command command, CancellationToken cancellationTo Domain.Models.Vlan vlan; - using (var lockResult = await _lockService.GetPartitionLock(partitionId.Value).LockAsync(10000)) + using (var lockResult = await lockService.GetPartitionLock(partitionId.Value).LockAsync(10000)) { if (!lockResult.AcquiredLock) throw new ConflictException("Could not acquire Partition lock"); - var query = _db.Vlans + var query = dbContext.Vlans .Where(x => x.PartitionId == partitionId.Value && !x.InUse && @@ -149,10 +131,10 @@ public async Task Handle(Command command, CancellationToken cancellationTo throw new ConflictException("No VLANs available"); vlan.InUse = true; - await _db.SaveChangesAsync(cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); } - return _mapper.Map(vlan); + return mapper.Map(vlan); } } } \ No newline at end of file diff --git a/src/Caster.Api/Features/Vlan/Requests/Vlans/AddVlansToPartition.cs b/src/Caster.Api/Features/Vlan/Requests/Vlans/AddVlansToPartition.cs index 4353f9c..3524de4 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Vlans/AddVlansToPartition.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Vlans/AddVlansToPartition.cs @@ -20,6 +20,8 @@ using FluentValidation; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan; @@ -53,46 +55,30 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + ILockService lockService) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly ILockService _lockService; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _lockService = lockService; - } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public async Task Handle(Command command, CancellationToken cancellationToken) + public override async Task HandleRequest(Command command, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var partition = await _db.Partitions + var partition = await dbContext.Partitions .Where(x => x.Id == command.PartitionId) .FirstOrDefaultAsync(cancellationToken); Domain.Models.Vlan[] vlans; // Lock poolId as proxy for unassigned partition of that pool - using (var lockResult = await _lockService.GetPartitionLock(partition.PoolId).LockAsync(10000)) + using (var lockResult = await lockService.GetPartitionLock(partition.PoolId).LockAsync(10000)) { if (!lockResult.AcquiredLock) throw new ConflictException("Could not acquire Partition lock"); - var query = _db.Vlans + var query = dbContext.Vlans .Where(x => x.PoolId == partition.PoolId && !x.PartitionId.HasValue && @@ -121,10 +107,10 @@ public async Task Handle(Command command, CancellationToken cancellation vlan.PartitionId = command.PartitionId; } - await _db.SaveChangesAsync(cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); } - return _mapper.Map(vlans); + return mapper.Map(vlans); } } } \ No newline at end of file diff --git a/src/Caster.Api/Features/Vlan/Requests/Vlans/GetVlan.cs b/src/Caster.Api/Features/Vlan/Requests/Vlans/GetVlan.cs index dc4f73f..7f067eb 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Vlans/GetVlan.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Vlans/GetVlan.cs @@ -4,19 +4,17 @@ using System; using System.Linq; using System.Runtime.Serialization; -using System.Security.Claims; -using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Microsoft.EntityFrameworkCore; using AutoMapper.QueryableExtensions; +using Caster.Api.Domain.Models; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Vlan { @@ -32,34 +30,17 @@ public class Query : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var vlan = await _db.Vlans + var vlan = await dbContext.Vlans .Where(x => x.Id == request.Id) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(cancellationToken); if (vlan == null) throw new EntityNotFoundException(); diff --git a/src/Caster.Api/Features/Vlan/Requests/Vlans/GetVlans.cs b/src/Caster.Api/Features/Vlan/Requests/Vlans/GetVlans.cs index 029aff3..7a494d0 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Vlans/GetVlans.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Vlans/GetVlans.cs @@ -20,6 +20,8 @@ using Caster.Api.Features.Shared.Services; using FluentValidation; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan { @@ -44,31 +46,14 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query query, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query query, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var vlanQuery = _db.Vlans.AsQueryable(); + var vlanQuery = dbContext.Vlans.AsQueryable(); if (query.PartitionId.HasValue) { @@ -81,8 +66,8 @@ public async Task Handle(Query query, CancellationToken cancellationToke } return await vlanQuery - .ProjectTo(_mapper.ConfigurationProvider) - .ToArrayAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToArrayAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Vlan/Requests/Vlans/PartialEditVlan.cs b/src/Caster.Api/Features/Vlan/Requests/Vlans/PartialEditVlan.cs index 13f8f0b..6e58701 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Vlans/PartialEditVlan.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Vlans/PartialEditVlan.cs @@ -8,18 +8,15 @@ using Caster.Api.Data; using AutoMapper; using System.Runtime.Serialization; -using Microsoft.AspNetCore.Authorization; -using Caster.Api.Infrastructure.Identity; -using CodeAnalysis = Microsoft.CodeAnalysis; using System.Text.Json.Serialization; -using System.Security.Claims; using System.Linq; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Exceptions; using Microsoft.EntityFrameworkCore; using FluentValidation; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan { @@ -51,39 +48,21 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command command, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - - public async Task Handle(Command command, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var vlan = await _db.Vlans + var vlan = await dbContext.Vlans .Where(x => x.Id == command.Id) .FirstOrDefaultAsync(cancellationToken); - _mapper.Map(command, vlan); - await _db.SaveChangesAsync(cancellationToken); + mapper.Map(command, vlan); + await dbContext.SaveChangesAsync(cancellationToken); - return _mapper.Map(vlan); + return mapper.Map(vlan); } } } diff --git a/src/Caster.Api/Features/Vlan/Requests/Vlans/ReassignVlans.cs b/src/Caster.Api/Features/Vlan/Requests/Vlans/ReassignVlans.cs index 30fa684..824b3c6 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Vlans/ReassignVlans.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Vlans/ReassignVlans.cs @@ -4,24 +4,22 @@ using System; using System.Linq; using System.Runtime.Serialization; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Caster.Api.Domain.Services; -using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; using FluentValidation; using System.Collections.Generic; using Caster.Api.Utilities.Synchronization; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan; @@ -57,37 +55,16 @@ public Validator(IValidationService validationService) RuleFor(x => x.FromPartitionId) .Must((command, fromPartitionId) => fromPartitionId != command.ToPartitionId) .WithMessage("Cannot reassign VLANs to their existing Partition"); - } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext, ILockService lockService) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly ILockService _lockService; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _lockService = lockService; - } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public async Task Handle(Command command, CancellationToken cancellationToken) + public override async Task HandleRequest(Command command, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - Domain.Models.Vlan[] vlans; // Order partitions by id so we always obtain locks in the same order to avoid deadlocking @@ -96,7 +73,7 @@ public async Task Handle(Command command, CancellationToken cancellation foreach (var lockId in lockIds) { - locks.Add(_lockService.GetPartitionLock(lockId)); + locks.Add(lockService.GetPartitionLock(lockId)); } using (var firstLockResult = await locks[0].LockAsync(10000)) @@ -105,7 +82,7 @@ public async Task Handle(Command command, CancellationToken cancellation if (!firstLockResult.AcquiredLock || !secondLockResult.AcquiredLock) throw new ConflictException("Could not acquire Partition lock"); - vlans = await _db.Vlans + vlans = await dbContext.Vlans .Where(x => command.VlanIds.Contains(x.Id)) .ToArrayAsync(cancellationToken); @@ -117,10 +94,10 @@ public async Task Handle(Command command, CancellationToken cancellation vlan.PartitionId = command.ToPartitionId; } - await _db.SaveChangesAsync(cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); } - return _mapper.Map(vlans); + return mapper.Map(vlans); } } } \ No newline at end of file diff --git a/src/Caster.Api/Features/Vlan/Requests/Vlans/ReleaseVlan.cs b/src/Caster.Api/Features/Vlan/Requests/Vlans/ReleaseVlan.cs index 43734be..5ffd4cc 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Vlans/ReleaseVlan.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Vlans/ReleaseVlan.cs @@ -4,22 +4,21 @@ using System; using System.Linq; using System.Runtime.Serialization; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Caster.Api.Domain.Services; using Microsoft.EntityFrameworkCore; using System.Text.Json.Serialization; using FluentValidation; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan; @@ -43,50 +42,30 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext, ILockService lockService) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly ILockService _lockService; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService) + public override async Task HandleRequest(Command command, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _lockService = lockService; - } - - public async Task Handle(Command command, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var vlan = await _db.Vlans + var vlan = await dbContext.Vlans .Where(x => x.Id == command.Id) .FirstOrDefaultAsync(cancellationToken); if (!vlan.PartitionId.HasValue) throw new ConflictException("Only VLANs assigned to a Partition can be acquired or released."); - using (var lockResult = await _lockService.GetPartitionLock(vlan.PartitionId.Value).LockAsync(10000)) + using (var lockResult = await lockService.GetPartitionLock(vlan.PartitionId.Value).LockAsync(10000)) { if (!lockResult.AcquiredLock) throw new ConflictException("Could not acquire Partition lock"); vlan.InUse = false; - await _db.SaveChangesAsync(cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); } - return _mapper.Map(vlan); + return mapper.Map(vlan); } } } diff --git a/src/Caster.Api/Features/Vlan/Requests/Vlans/RemoveVlansFromPartition.cs b/src/Caster.Api/Features/Vlan/Requests/Vlans/RemoveVlansFromPartition.cs index 33c9f37..4129430 100644 --- a/src/Caster.Api/Features/Vlan/Requests/Vlans/RemoveVlansFromPartition.cs +++ b/src/Caster.Api/Features/Vlan/Requests/Vlans/RemoveVlansFromPartition.cs @@ -4,22 +4,21 @@ using System; using System.Linq; using System.Runtime.Serialization; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using MediatR; using AutoMapper; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Data; using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.Exceptions; -using Caster.Api.Infrastructure.Identity; using Caster.Api.Domain.Services; using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; using FluentValidation; using Caster.Api.Features.Shared.Services; using Caster.Api.Infrastructure.Extensions; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Vlan; @@ -53,41 +52,21 @@ public Validator(IValidationService validationService) } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext, ILockService lockService) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly ILockService _lockService; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageVLANs], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService) + public override async Task HandleRequest(Command command, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _lockService = lockService; - } - - public async Task Handle(Command command, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - Domain.Models.Vlan[] vlans; - using (var lockResult = await _lockService.GetPartitionLock(command.PartitionId).LockAsync(10000)) + using (var lockResult = await lockService.GetPartitionLock(command.PartitionId).LockAsync(10000)) { if (!lockResult.AcquiredLock) throw new ConflictException("Could not acquire Partition lock"); - var query = _db.Vlans + var query = dbContext.Vlans .Where(x => x.PartitionId == command.PartitionId && !x.InUse); @@ -118,10 +97,10 @@ public async Task Handle(Command command, CancellationToken cancellation vlan.PartitionId = null; } - await _db.SaveChangesAsync(cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); } - return _mapper.Map(vlans); + return mapper.Map(vlans); } } } \ No newline at end of file diff --git a/src/Caster.Api/Features/Workspaces/Requests/Create.cs b/src/Caster.Api/Features/Workspaces/Requests/Create.cs index e175d72..859032c 100644 --- a/src/Caster.Api/Features/Workspaces/Requests/Create.cs +++ b/src/Caster.Api/Features/Workspaces/Requests/Create.cs @@ -8,11 +8,7 @@ using Caster.Api.Data; using AutoMapper; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using Caster.Api.Features.Workspaces.Interfaces; using FluentValidation; using Caster.Api.Infrastructure.Options; @@ -20,6 +16,7 @@ using Caster.Api.Infrastructure.Extensions; using Caster.Api.Features.Shared; using Caster.Api.Features.Shared.Validators; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Workspaces { @@ -54,14 +51,14 @@ public class Command : IRequest, IWorkspaceUpdateRequest public string TerraformVersion { get; set; } /// - /// Limit the number of concurrent operations as Terraform walks the graph. + /// Limit the number of concurrent operations as Terraform walks the graph. /// If null, the Terraform default will be used. /// [DataMember] public int? Parallelism { get; set; } /// - /// If set, the number of consecutive failed destroys in an Azure Workspace before + /// If set, the number of consecutive failed destroys in an Azure Workspace before /// Caster will attempt to mitigate by removing azurerm_resource_group children from the state. /// [DataMember] @@ -82,39 +79,23 @@ public CommandValidator(IValidationService validationService, TerraformOptions o } } - public class Handler : IRequestHandler + public class Handler( + ICasterAuthorizationService authorizationService, + IMapper mapper, + CasterContext dbContext, + TerraformOptions terraformOptions) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly TerraformOptions _terraformOptions; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - TerraformOptions terraformOptions) - { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _terraformOptions = terraformOptions; - } + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.DirectoryId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public async Task Handle(Command request, CancellationToken cancellationToken) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var workspace = _mapper.Map(request); + var workspace = mapper.Map(request); workspace = await SetCascadedProperties(workspace, request, cancellationToken); - _db.Workspaces.Add(workspace); - await _db.SaveChangesAsync(cancellationToken); - return _mapper.Map(workspace); + dbContext.Workspaces.Add(workspace); + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(workspace); } private async Task SetCascadedProperties(Domain.Models.Workspace workspace, Command request, CancellationToken ct) @@ -122,7 +103,7 @@ public async Task Handle(Command request, CancellationToken cancellat if (!request.Parallelism.HasValue && string.IsNullOrEmpty(request.TerraformVersion)) { // Load parent directories from database - var directory = await _db.GetDirectoryWithAncestors(workspace.DirectoryId, ct); + var directory = await dbContext.GetDirectoryWithAncestors(workspace.DirectoryId, ct); workspace.TerraformVersion = !string.IsNullOrEmpty(request.TerraformVersion) ? request.TerraformVersion : @@ -152,7 +133,7 @@ private string GetTerraformVersion(Domain.Models.Directory directory) } else { - return _terraformOptions.DefaultVersion; + return terraformOptions.DefaultVersion; } } @@ -188,7 +169,7 @@ private string GetTerraformVersion(Domain.Models.Directory directory) } else { - return _terraformOptions.AzureDestroyFailureThreshhold; + return terraformOptions.AzureDestroyFailureThreshhold; } } } diff --git a/src/Caster.Api/Features/Workspaces/Requests/Delete.cs b/src/Caster.Api/Features/Workspaces/Requests/Delete.cs index 876c0c0..78031a8 100644 --- a/src/Caster.Api/Features/Workspaces/Requests/Delete.cs +++ b/src/Caster.Api/Features/Workspaces/Requests/Delete.cs @@ -6,62 +6,37 @@ using System.Threading; using System.Threading.Tasks; using MediatR; -using AutoMapper; using Caster.Api.Data; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; -using Caster.Api.Domain.Events; -using Caster.Api.Features.Workspaces.Interfaces; using Caster.Api.Domain.Services; +using Caster.Api.Domain.Models; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Workspaces { public class Delete { [DataContract(Name = "DeleteWorkspaceCommand")] - public class Command : IRequest, IWorkspaceDeleteRequest + public class Command : IRequest { public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, CasterContext dbContext, ILockService lockService) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly ILockService _lockService; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _lockService = lockService; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var workspace = await _db.Workspaces.FindAsync(request.Id); + var workspace = await dbContext.Workspaces.FindAsync([request.Id], cancellationToken); if (workspace == null) throw new EntityNotFoundException(); - using (var lockResult = await _lockService.GetWorkspaceLock(request.Id).LockAsync(0)) + using (var lockResult = await lockService.GetWorkspaceLock(request.Id).LockAsync(0)) { if (!lockResult.AcquiredLock) throw new WorkspaceConflictException(); @@ -69,11 +44,11 @@ public async Task Handle(Command request, CancellationToken cancellationToken) if (workspace.GetState().GetResources().Any()) throw new ConflictException("Cannot delete a Workspace with deployed Resources."); - if (await _db.AnyIncompleteRuns(request.Id)) + if (await dbContext.AnyIncompleteRuns(request.Id)) throw new ConflictException("Cannot delete a Workspace with pending Runs."); - _db.Workspaces.Remove(workspace); - await _db.SaveChangesAsync(cancellationToken); + dbContext.Workspaces.Remove(workspace); + await dbContext.SaveChangesAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Workspaces/Requests/Edit.cs b/src/Caster.Api/Features/Workspaces/Requests/Edit.cs index 2840070..1902edc 100644 --- a/src/Caster.Api/Features/Workspaces/Requests/Edit.cs +++ b/src/Caster.Api/Features/Workspaces/Requests/Edit.cs @@ -10,17 +10,14 @@ using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Models; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using Caster.Api.Features.Workspaces.Interfaces; using FluentValidation; using Caster.Api.Infrastructure.Extensions; using Caster.Api.Features.Shared.Services; using Caster.Api.Features.Shared.Validators; using Caster.Api.Infrastructure.Options; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Workspaces { @@ -85,39 +82,22 @@ public CommandValidator(IValidationService validationService, TerraformOptions o } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.DirectoryId, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var workspace = await _db.Workspaces.FindAsync(request.Id); + var workspace = await dbContext.Workspaces.FindAsync([request.Id], cancellationToken); if (workspace == null) throw new EntityNotFoundException(); - _mapper.Map(request, workspace); + mapper.Map(request, workspace); - await _db.SaveChangesAsync(); - return _mapper.Map(workspace); + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(workspace); } } } diff --git a/src/Caster.Api/Features/Workspaces/Requests/Get.cs b/src/Caster.Api/Features/Workspaces/Requests/Get.cs index 70b5014..53e283a 100644 --- a/src/Caster.Api/Features/Workspaces/Requests/Get.cs +++ b/src/Caster.Api/Features/Workspaces/Requests/Get.cs @@ -11,17 +11,15 @@ using AutoMapper.QueryableExtensions; using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Workspaces { public class Get { - [DataContract(Name="GetWorkspaceQuery")] + [DataContract(Name = "GetWorkspaceQuery")] public class Query : IRequest { /// @@ -31,32 +29,15 @@ public class Query : IRequest public Guid Id { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var workspace = await _db.Workspaces - .ProjectTo(_mapper.ConfigurationProvider) + var workspace = await dbContext.Workspaces + .ProjectTo(mapper.ConfigurationProvider) .SingleOrDefaultAsync(x => x.Id == request.Id, cancellationToken); if (workspace == null) diff --git a/src/Caster.Api/Features/Workspaces/Requests/GetAll.cs b/src/Caster.Api/Features/Workspaces/Requests/GetAll.cs index d42724f..a918d31 100644 --- a/src/Caster.Api/Features/Workspaces/Requests/GetAll.cs +++ b/src/Caster.Api/Features/Workspaces/Requests/GetAll.cs @@ -9,48 +9,28 @@ using Microsoft.EntityFrameworkCore; using AutoMapper.QueryableExtensions; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Workspaces { public class GetAll { - [DataContract(Name="GetWorkspacesQuery")] + [DataContract(Name = "GetWorkspacesQuery")] public class Query : IRequest { } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewProjects], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - return await _db.Workspaces - .ProjectTo(_mapper.ConfigurationProvider) + return await dbContext.Workspaces + .ProjectTo(mapper.ConfigurationProvider) .ToArrayAsync(); } } diff --git a/src/Caster.Api/Features/Workspaces/Requests/GetByDirectory.cs b/src/Caster.Api/Features/Workspaces/Requests/GetByDirectory.cs index 465df95..d117641 100644 --- a/src/Caster.Api/Features/Workspaces/Requests/GetByDirectory.cs +++ b/src/Caster.Api/Features/Workspaces/Requests/GetByDirectory.cs @@ -11,19 +11,18 @@ using Microsoft.EntityFrameworkCore; using AutoMapper.QueryableExtensions; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Models; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; +using Caster.Api.Features.Shared; +using FluentValidation; +using Caster.Api.Features.Shared.Services; +using Caster.Api.Infrastructure.Extensions; namespace Caster.Api.Features.Workspaces { public class GetByDirectory { - [DataContract(Name="GetWorkspacesByDirectoryQuery")] + [DataContract(Name = "GetWorkspacesByDirectoryQuery")] public class Query : IRequest { /// @@ -33,44 +32,25 @@ public class Query : IRequest public Guid DirectoryId { get; set; } } - public class Handler : IRequestHandler + public class Validator : AbstractValidator { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public Validator(IValidationService validationService) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); + RuleFor(x => x.DirectoryId).DirectoryExists(validationService); } + } - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - await ValidateEntities(request.DirectoryId); - - return await _db.Workspaces - .Where(x => x.DirectoryId == request.DirectoryId) - .ProjectTo(_mapper.ConfigurationProvider) - .ToArrayAsync(); - } + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler + { + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.DirectoryId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], cancellationToken); - private async Task ValidateEntities(Guid directoryId) + public override async Task HandleRequest(Query request, CancellationToken cancellationToken) { - var directory = await _db.Directories.FindAsync(directoryId); - - if (directory == null) - throw new EntityNotFoundException(); + return await dbContext.Workspaces + .Where(x => x.DirectoryId == request.DirectoryId) + .ProjectTo(mapper.ConfigurationProvider) + .ToArrayAsync(cancellationToken); } } } diff --git a/src/Caster.Api/Features/Workspaces/Requests/GetLockingStatus.cs b/src/Caster.Api/Features/Workspaces/Requests/GetLockingStatus.cs index a2d85ef..1f03f92 100644 --- a/src/Caster.Api/Features/Workspaces/Requests/GetLockingStatus.cs +++ b/src/Caster.Api/Features/Workspaces/Requests/GetLockingStatus.cs @@ -5,42 +5,26 @@ using System.Threading.Tasks; using MediatR; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using Caster.Api.Domain.Services; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Workspaces { public class GetLockingStatus { - [DataContract(Name="GetWorkspaceLockingStatusQuery")] - public class Query : IRequest {} + [DataContract(Name = "GetWorkspaceLockingStatusQuery")] + public class Query : IRequest { } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, ILockService lockService) : BaseHandler { - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly ILockService _lockService; + public override async Task Authorize(Query request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ViewWorkspaces], cancellationToken); - public Handler( - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService) + public override Task HandleRequest(Query request, CancellationToken cancellationToken) { - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _lockService = lockService; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - return _lockService.IsWorkspaceLockingEnabled(); + return Task.FromResult(lockService.IsWorkspaceLockingEnabled()); } } } diff --git a/src/Caster.Api/Features/Workspaces/Requests/PartialEdit.cs b/src/Caster.Api/Features/Workspaces/Requests/PartialEdit.cs index 4adca42..54f02b3 100644 --- a/src/Caster.Api/Features/Workspaces/Requests/PartialEdit.cs +++ b/src/Caster.Api/Features/Workspaces/Requests/PartialEdit.cs @@ -10,11 +10,8 @@ using System.Runtime.Serialization; using Caster.Api.Infrastructure.Exceptions; using Caster.Api.Domain.Models; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Microsoft.CodeAnalysis; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using Caster.Api.Features.Workspaces.Interfaces; using FluentValidation; using Caster.Api.Features.Shared.Services; @@ -22,6 +19,7 @@ using System.Text.Json.Serialization; using Caster.Api.Features.Shared.Validators; using Caster.Api.Infrastructure.Options; +using Caster.Api.Features.Shared; namespace Caster.Api.Features.Workspaces { @@ -59,14 +57,14 @@ public class Command : IRequest, IWorkspaceUpdateRequest public string TerraformVersion { get; set; } /// - /// Limit the number of concurrent operations as Terraform walks the graph. + /// Limit the number of concurrent operations as Terraform walks the graph. /// If null, the Terraform default will be used. /// [DataMember] public Optional Parallelism { get; set; } /// - /// If set, the number of consecutive failed destroys in an Azure Workspace before + /// If set, the number of consecutive failed destroys in an Azure Workspace before /// Caster will attempt to mitigate by removing azurerm_resource_group children from the state. /// [DataMember] @@ -87,39 +85,22 @@ public CommandValidator(IValidationService validationService, TerraformOptions o } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, IMapper mapper, CasterContext dbContext) : BaseHandler { - private readonly CasterContext _db; - private readonly IMapper _mapper; - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize(request.Id, [SystemPermission.EditProjects], [ProjectPermission.EditProject], cancellationToken); - public Handler( - CasterContext db, - IMapper mapper, - IAuthorizationService authorizationService, - IIdentityResolver identityResolver) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _db = db; - _mapper = mapper; - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new ContentDeveloperRequirement())).Succeeded) - throw new ForbiddenException(); - - var workspace = await _db.Workspaces.FindAsync(request.Id); + var workspace = await dbContext.Workspaces.FindAsync([request.Id], cancellationToken); if (workspace == null) throw new EntityNotFoundException(); - _mapper.Map(request, workspace); + mapper.Map(request, workspace); - await _db.SaveChangesAsync(cancellationToken); - return _mapper.Map(workspace); + await dbContext.SaveChangesAsync(cancellationToken); + return mapper.Map(workspace); } } } diff --git a/src/Caster.Api/Features/Workspaces/Requests/SetLockingStatus.cs b/src/Caster.Api/Features/Workspaces/Requests/SetLockingStatus.cs index 22c7346..89300c2 100644 --- a/src/Caster.Api/Features/Workspaces/Requests/SetLockingStatus.cs +++ b/src/Caster.Api/Features/Workspaces/Requests/SetLockingStatus.cs @@ -5,59 +5,40 @@ using System.Threading.Tasks; using MediatR; using System.Runtime.Serialization; -using Caster.Api.Infrastructure.Exceptions; -using System.Security.Claims; -using Microsoft.AspNetCore.Authorization; using Caster.Api.Infrastructure.Authorization; -using Caster.Api.Infrastructure.Identity; using Caster.Api.Domain.Services; using Caster.Api.Domain.Events; +using Caster.Api.Features.Shared; +using Caster.Api.Domain.Models; namespace Caster.Api.Features.Workspaces { public class SetLockingStatus { - [DataContract(Name="SetWorkspaceLockingStatusQuery")] + [DataContract(Name = "SetWorkspaceLockingStatusQuery")] public class Command : IRequest { public bool Enabled { get; set; } } - public class Handler : IRequestHandler + public class Handler(ICasterAuthorizationService authorizationService, ILockService lockService, IMediator mediator) : BaseHandler { - private readonly IAuthorizationService _authorizationService; - private readonly ClaimsPrincipal _user; - private readonly ILockService _lockService; - private readonly IMediator _mediator; + public override async Task Authorize(Command request, CancellationToken cancellationToken) => + await authorizationService.Authorize([SystemPermission.ManageWorkspaces], cancellationToken); - public Handler( - IAuthorizationService authorizationService, - IIdentityResolver identityResolver, - ILockService lockService, - IMediator mediator) + public override async Task HandleRequest(Command request, CancellationToken cancellationToken) { - _authorizationService = authorizationService; - _user = identityResolver.GetClaimsPrincipal(); - _lockService = lockService; - _mediator = mediator; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - if (!(await _authorizationService.AuthorizeAsync(_user, null, new FullRightsRequirement())).Succeeded) - throw new ForbiddenException(); - if (request.Enabled) { - _lockService.EnableWorkspaceLocking(); + lockService.EnableWorkspaceLocking(); } else { - _lockService.DisableWorkspaceLocking(); + lockService.DisableWorkspaceLocking(); } - var lockingEnabled = _lockService.IsWorkspaceLockingEnabled(); - await _mediator.Publish(new WorkspaceSettingsUpdated(lockingEnabled)); + var lockingEnabled = lockService.IsWorkspaceLockingEnabled(); + await mediator.Publish(new WorkspaceSettingsUpdated(lockingEnabled)); return lockingEnabled; } diff --git a/src/Caster.Api/Hubs/HubGroups.cs b/src/Caster.Api/Hubs/HubGroups.cs index 3e16ba4..fd8f6f9 100644 --- a/src/Caster.Api/Hubs/HubGroups.cs +++ b/src/Caster.Api/Hubs/HubGroups.cs @@ -6,6 +6,8 @@ namespace Caster.Api.Hubs public enum HubGroups { WorkspacesAdmin, - VlansAdmin + VlansAdmin, + RolesAdmin, + ProjectsAdmin } } \ No newline at end of file diff --git a/src/Caster.Api/Hubs/ProjectHub.cs b/src/Caster.Api/Hubs/ProjectHub.cs index 066c8e5..cdc57ad 100644 --- a/src/Caster.Api/Hubs/ProjectHub.cs +++ b/src/Caster.Api/Hubs/ProjectHub.cs @@ -8,28 +8,35 @@ using System.Threading; using System.Threading.Tasks; using Caster.Api.Data; +using Caster.Api.Domain.Models; using Caster.Api.Domain.Services; using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Infrastructure.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; namespace Caster.Api.Hubs; -[Authorize(Policy = nameof(CasterClaimTypes.ContentDeveloper))] +[Authorize] public class ProjectHub : Hub { private readonly IOutputService _outputService; private readonly CasterContext _db; + private readonly ICasterAuthorizationService _authorizationService; - public ProjectHub(IOutputService outputService, CasterContext db) + public ProjectHub(IOutputService outputService, CasterContext db, ICasterAuthorizationService authorizationService) { _outputService = outputService; _db = db; + _authorizationService = authorizationService; } public async Task JoinProject(Guid projectId) { + if (!await _authorizationService.Authorize(projectId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], Context.ConnectionAborted)) + throw new ForbiddenException(); + await Groups.AddToGroupAsync(Context.ConnectionId, projectId.ToString()); } @@ -38,8 +45,37 @@ public async Task LeaveProject(Guid projectId) await Groups.RemoveFromGroupAsync(Context.ConnectionId, projectId.ToString()); } + public async Task JoinProjectAdmin(Guid projectId) + { + if (!await _authorizationService.Authorize(projectId, [SystemPermission.ViewProjects], [ProjectPermission.ManageProject], Context.ConnectionAborted)) + throw new ForbiddenException(); + + await Groups.AddToGroupAsync(Context.ConnectionId, ProjectHubMethods.GetProjectAdminGroup(projectId)); + } + + public async Task LeaveProjectAdmin(Guid projectId) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, ProjectHubMethods.GetProjectAdminGroup(projectId)); + } + + public async Task JoinGroup(Guid groupId) + { + if (!await _authorizationService.Authorize([SystemPermission.ViewGroups], Context.ConnectionAborted)) + throw new ForbiddenException(); + + await Groups.AddToGroupAsync(Context.ConnectionId, groupId.ToString()); + } + + public async Task LeaveGroup(Guid groupId) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupId.ToString()); + } + public async Task JoinWorkspace(Guid workspaceId) { + if (!await _authorizationService.Authorize(workspaceId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], Context.ConnectionAborted)) + throw new ForbiddenException(); + await Groups.AddToGroupAsync(Context.ConnectionId, workspaceId.ToString()); } @@ -50,6 +86,9 @@ public async Task LeaveWorkspace(Guid workspaceId) public async Task JoinDesign(Guid designId) { + if (!await _authorizationService.Authorize(designId, [SystemPermission.ViewProjects], [ProjectPermission.ViewProject], Context.ConnectionAborted)) + throw new ForbiddenException(); + await Groups.AddToGroupAsync(Context.ConnectionId, designId.ToString()); } @@ -58,30 +97,45 @@ public async Task LeaveDesign(Guid designId) await Groups.RemoveFromGroupAsync(Context.ConnectionId, designId.ToString()); } - [Authorize(Policy = nameof(CasterClaimTypes.SystemAdmin))] public async Task JoinWorkspacesAdmin() { + if (!await _authorizationService.Authorize([SystemPermission.ViewWorkspaces], Context.ConnectionAborted)) + throw new ForbiddenException(); + await Groups.AddToGroupAsync(Context.ConnectionId, nameof(HubGroups.WorkspacesAdmin)); } - [Authorize(Policy = nameof(CasterClaimTypes.SystemAdmin))] public async Task LeaveWorkspacesAdmin() { await Groups.RemoveFromGroupAsync(Context.ConnectionId, nameof(HubGroups.WorkspacesAdmin)); } - [Authorize(Policy = nameof(CasterClaimTypes.SystemAdmin))] public async Task JoinVlansAdmin() { + if (!await _authorizationService.Authorize([SystemPermission.ViewVLANs], Context.ConnectionAborted)) + throw new ForbiddenException(); + await Groups.AddToGroupAsync(Context.ConnectionId, nameof(HubGroups.VlansAdmin)); } - [Authorize(Policy = nameof(CasterClaimTypes.SystemAdmin))] public async Task LeaveVlansAdmin() { await Groups.RemoveFromGroupAsync(Context.ConnectionId, nameof(HubGroups.VlansAdmin)); } + public async Task JoinRolesAdmin() + { + if (!await _authorizationService.Authorize([SystemPermission.ViewRoles], Context.ConnectionAborted)) + throw new ForbiddenException(); + + await Groups.AddToGroupAsync(Context.ConnectionId, nameof(HubGroups.RolesAdmin)); + } + + public async Task LeaveRolesAdmin() + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, nameof(HubGroups.RolesAdmin)); + } + #region RunOutput enum OutputType @@ -159,6 +213,9 @@ private async Task GetDbOutput(Guid id, OutputType type, CancellationTok public async IAsyncEnumerable GetPlanOutput(Guid id, [EnumeratorCancellation] CancellationToken cancellationToken) { + if (!await _authorizationService.Authorize(id, [SystemPermission.ViewProjects, SystemPermission.ViewWorkspaces], [ProjectPermission.ViewProject], Context.ConnectionAborted)) + throw new ForbiddenException(); + await foreach (var output in this.GetOutput(id, OutputType.Plan, cancellationToken)) { yield return output; @@ -167,6 +224,9 @@ public async IAsyncEnumerable GetPlanOutput(Guid id, [EnumeratorCancella public async IAsyncEnumerable GetApplyOutput(Guid id, [EnumeratorCancellation] CancellationToken cancellationToken) { + if (!await _authorizationService.Authorize(id, [SystemPermission.ViewProjects, SystemPermission.ViewWorkspaces], [ProjectPermission.ViewProject], Context.ConnectionAborted)) + throw new ForbiddenException(); + await foreach (var output in this.GetOutput(id, OutputType.Apply, cancellationToken)) { yield return output; @@ -181,7 +241,23 @@ public static class ProjectHubMethods public const string DesignCreated = "DesignCreated"; public const string DesignUpdated = "DesignUpdated"; public const string DesignDeleted = "DesignDeleted"; + public const string VariableCreated = "VariableCreated"; public const string VariableUpdated = "VariableUpdated"; public const string VariableDeleted = "VariableDeleted"; + + public const string GroupMembershipCreated = nameof(GroupMembershipCreated); + public const string GroupMembershipUpdated = nameof(GroupMembershipUpdated); + public const string GroupMembershipDeleted = nameof(GroupMembershipDeleted); + + public const string ProjectMembershipCreated = nameof(ProjectMembershipCreated); + public const string ProjectMembershipUpdated = nameof(ProjectMembershipUpdated); + public const string ProjectMembershipDeleted = nameof(ProjectMembershipDeleted); + + private const string GroupSeparator = "-"; + + public static string GetProjectAdminGroup(Guid projectId) + { + return $"{HubGroups.ProjectsAdmin}{GroupSeparator}{projectId}"; + } } diff --git a/src/Caster.Api/Infrastructure/Authorization/AuthorizationConstants.cs b/src/Caster.Api/Infrastructure/Authorization/AuthorizationConstants.cs new file mode 100644 index 0000000..a88ecea --- /dev/null +++ b/src/Caster.Api/Infrastructure/Authorization/AuthorizationConstants.cs @@ -0,0 +1,10 @@ +using System; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Infrastructure.Authorization; + +public static class AuthorizationConstants +{ + public const string PermissionsClaimType = "Permission"; + public const string ProjectPermissionsClaimType = "ProjectPermission"; +} \ No newline at end of file diff --git a/src/Caster.Api/Infrastructure/Authorization/AuthorizationService.cs b/src/Caster.Api/Infrastructure/Authorization/AuthorizationService.cs new file mode 100644 index 0000000..bf53b56 --- /dev/null +++ b/src/Caster.Api/Infrastructure/Authorization/AuthorizationService.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Caster.Api.Data; +using Caster.Api.Domain.Models; +using Caster.Api.Infrastructure.Exceptions; +using Caster.Api.Infrastructure.Identity; +using Microsoft.AspNetCore.Authorization; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.EntityFrameworkCore; + +namespace Caster.Api.Infrastructure.Authorization; + +public interface ICasterAuthorizationService +{ + Task Authorize( + SystemPermission[] requiredSystemPermissions, + CancellationToken cancellationToken); + + Task Authorize( + Guid? resourceId, + SystemPermission[] requiredSystemPermissions, + ProjectPermission[] requiredProjectPermissions, + CancellationToken cancellationToken) where T : IEntity; + + IEnumerable GetAuthorizedProjectIds(); + IEnumerable GetSystemPermissions(); + IEnumerable GetProjectPermissions(Guid? projectId = null); +} + +public class AuthorizationService( + IAuthorizationService authService, + IIdentityResolver identityResolver, + CasterContext dbContext) : ICasterAuthorizationService +{ + public async Task Authorize( + SystemPermission[] requiredSystemPermissions, + CancellationToken cancellationToken) + { + return await Authorize(null, requiredSystemPermissions, [], cancellationToken); + } + + public async Task Authorize( + Guid? resourceId, + SystemPermission[] requiredSystemPermissions, + ProjectPermission[] requiredProjectPermissions, + CancellationToken cancellationToken) where T : IEntity + { + bool succeeded = false; + var claimsPrincipal = identityResolver.GetClaimsPrincipal(); + var permissionRequirement = new SystemPermissionRequirement(requiredSystemPermissions); + var permissionResult = await authService.AuthorizeAsync(claimsPrincipal, null, permissionRequirement); + + if (permissionResult.Succeeded) + succeeded = true; + + if (!succeeded && resourceId.HasValue) + { + var projectId = await GetProjectId(resourceId.Value, cancellationToken); + + if (projectId == null) + { + succeeded = false; + } + else + { + var projectPermissionRequirement = new ProjectPermissionRequirement(requiredProjectPermissions, projectId.Value); + var projectPermissionResult = await authService.AuthorizeAsync(claimsPrincipal, null, projectPermissionRequirement); + + succeeded = projectPermissionResult.Succeeded; + } + + } + + return succeeded; + } + + public IEnumerable GetAuthorizedProjectIds() + { + return identityResolver.GetClaimsPrincipal().Claims + .Where(x => x.Type == AuthorizationConstants.ProjectPermissionsClaimType) + .Select(x => ProjectPermissionsClaim.FromString(x.Value).ProjectId) + .ToList(); + } + + public IEnumerable GetSystemPermissions() + { + return identityResolver.GetClaimsPrincipal().Claims + .Where(x => x.Type == AuthorizationConstants.PermissionsClaimType) + .Select(x => + { + if (Enum.TryParse(x.Value, out var permission)) + return permission; + + return (SystemPermission?)null; + }) + .Where(x => x.HasValue) + .Select(x => x.Value) + .ToList(); + } + + public IEnumerable GetProjectPermissions(Guid? projectId = null) + { + var permissions = identityResolver.GetClaimsPrincipal().Claims + .Where(x => x.Type == AuthorizationConstants.ProjectPermissionsClaimType) + .Select(x => ProjectPermissionsClaim.FromString(x.Value)); + + if (projectId.HasValue) + { + permissions = permissions.Where(x => x.ProjectId == projectId.Value); + } + + return permissions; + } + + private async Task GetProjectId(Guid resourceId, CancellationToken cancellationToken) + { + return typeof(T) switch + { + var t when t == typeof(Project) => resourceId, + var t when t == typeof(Directory) => await HandleDirectory(resourceId, cancellationToken), + var t when t == typeof(File) => await HandleFile(resourceId, cancellationToken), + var t when t == typeof(FileVersion) => await HandleFileVersion(resourceId, cancellationToken), + var t when t == typeof(Workspace) => await HandleWorkspace(resourceId, cancellationToken), + var t when t == typeof(Run) => await HandleRun(resourceId, cancellationToken), + var t when t == typeof(Plan) => await HandlePlan(resourceId, cancellationToken), + var t when t == typeof(Apply) => await HandleApply(resourceId, cancellationToken), + var t when t == typeof(Design) => await HandleDesign(resourceId, cancellationToken), + var t when t == typeof(DesignModule) => await HandleDesignModule(resourceId, cancellationToken), + var t when t == typeof(Variable) => await HandleVariable(resourceId, cancellationToken), + var t when t == typeof(ProjectMembership) => await HandleProjectMembership(resourceId, cancellationToken), + _ => throw new NotImplementedException($"Handler for type {typeof(T).Name} is not implemented.") + }; + } + + private async Task HandleDirectory(Guid id, CancellationToken cancellationToken) + { + return await dbContext.Directories + .Where(x => x.Id == id) + .Select(x => x.ProjectId) + .FirstOrDefaultAsync(cancellationToken); + } + + private async Task HandleFile(Guid id, CancellationToken cancellationToken) + { + return await dbContext.Files + .Where(x => x.Id == id) + .Select(x => x.Directory.ProjectId) + .FirstOrDefaultAsync(cancellationToken); + } + + private async Task HandleFileVersion(Guid id, CancellationToken cancellationToken) + { + return await dbContext.Files + .Where(x => x.Id == id) + .Select(x => x.Directory.ProjectId) + .FirstOrDefaultAsync(cancellationToken); + } + + private async Task HandleWorkspace(Guid id, CancellationToken cancellationToken) + { + return await dbContext.Workspaces + .Where(x => x.Id == id) + .Select(x => x.Directory.ProjectId) + .FirstOrDefaultAsync(cancellationToken); + } + + private async Task HandleRun(Guid id, CancellationToken cancellationToken) + { + return await dbContext.Runs + .Where(x => x.Id == id) + .Select(x => x.Workspace.Directory.ProjectId) + .FirstOrDefaultAsync(cancellationToken); + } + + private async Task HandlePlan(Guid id, CancellationToken cancellationToken) + { + return await dbContext.Plans + .Where(x => x.Id == id) + .Select(x => x.Run.Workspace.Directory.ProjectId) + .FirstOrDefaultAsync(cancellationToken); + } + + private async Task HandleApply(Guid id, CancellationToken cancellationToken) + { + return await dbContext.Applies + .Where(x => x.Id == id) + .Select(x => x.Run.Workspace.Directory.ProjectId) + .FirstOrDefaultAsync(cancellationToken); + } + + private async Task HandleDesign(Guid id, CancellationToken cancellationToken) + { + return await dbContext.Designs + .Where(x => x.Id == id) + .Select(x => x.Directory.ProjectId) + .FirstOrDefaultAsync(cancellationToken); + } + + private async Task HandleDesignModule(Guid id, CancellationToken cancellationToken) + { + return await dbContext.DesignModules + .Where(x => x.Id == id) + .Select(x => x.Design.Directory.ProjectId) + .FirstOrDefaultAsync(cancellationToken); + } + + private async Task HandleVariable(Guid id, CancellationToken cancellationToken) + { + return await dbContext.Variables + .Where(x => x.Id == id) + .Select(x => x.Design.Directory.ProjectId) + .FirstOrDefaultAsync(cancellationToken); + } + + private async Task HandleProjectMembership(Guid id, CancellationToken cancellationToken) + { + return await dbContext.ProjectMemberships + .Where(x => x.Id == id) + .Select(x => x.ProjectId) + .FirstOrDefaultAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/Caster.Api/Infrastructure/Authorization/ProjectPermissionClaim.cs b/src/Caster.Api/Infrastructure/Authorization/ProjectPermissionClaim.cs new file mode 100644 index 0000000..9c8f7e5 --- /dev/null +++ b/src/Caster.Api/Infrastructure/Authorization/ProjectPermissionClaim.cs @@ -0,0 +1,23 @@ +using System; +using System.Text.Json; +using Caster.Api.Domain.Models; + +namespace Caster.Api.Infrastructure.Authorization; + +public class ProjectPermissionsClaim +{ + public Guid ProjectId { get; set; } + public ProjectPermission[] Permissions { get; set; } = []; + + public ProjectPermissionsClaim() { } + + public static ProjectPermissionsClaim FromString(string json) + { + return JsonSerializer.Deserialize(json); + } + + public override string ToString() + { + return JsonSerializer.Serialize(this); + } +} \ No newline at end of file diff --git a/src/Caster.Api/Infrastructure/Authorization/ProjectPermissionRequirement.cs b/src/Caster.Api/Infrastructure/Authorization/ProjectPermissionRequirement.cs new file mode 100644 index 0000000..5281ced --- /dev/null +++ b/src/Caster.Api/Infrastructure/Authorization/ProjectPermissionRequirement.cs @@ -0,0 +1,70 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using Caster.Api.Domain.Models; +using Microsoft.AspNetCore.Authorization; +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Caster.Api.Infrastructure.Authorization +{ + public class ProjectPermissionRequirement : IAuthorizationRequirement + { + public ProjectPermission[] RequiredPermissions; + public Guid ProjectId; + + public ProjectPermissionRequirement( + ProjectPermission[] requiredPermissions, + Guid projectId) + { + RequiredPermissions = requiredPermissions; + ProjectId = projectId; + } + } + + public class ProjectPermissionsHandler : AuthorizationHandler, IAuthorizationHandler + { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ProjectPermissionRequirement requirement) + { + if (context.User == null) + { + context.Fail(); + } + else + { + ProjectPermissionsClaim projectPermissionsClaim = null; + + var claims = context.User.Claims + .Where(x => x.Type == AuthorizationConstants.ProjectPermissionsClaimType) + .ToList(); + + foreach (var claim in claims) + { + var claimValue = ProjectPermissionsClaim.FromString(claim.Value); + if (claimValue.ProjectId == requirement.ProjectId) + { + projectPermissionsClaim = claimValue; + break; + } + } + + if (projectPermissionsClaim == null) + { + context.Fail(); + } + else if (requirement.RequiredPermissions == null || requirement.RequiredPermissions.Length == 0) + { + context.Succeed(requirement); + } + else if (requirement.RequiredPermissions.Any(x => projectPermissionsClaim.Permissions.Contains(x))) + { + context.Succeed(requirement); + } + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Caster.Api/Infrastructure/Authorization/SystemPermissionRequirement.cs b/src/Caster.Api/Infrastructure/Authorization/SystemPermissionRequirement.cs new file mode 100644 index 0000000..d932add --- /dev/null +++ b/src/Caster.Api/Infrastructure/Authorization/SystemPermissionRequirement.cs @@ -0,0 +1,42 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +using Caster.Api.Domain.Models; +using Microsoft.AspNetCore.Authorization; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Caster.Api.Infrastructure.Authorization +{ + public class SystemPermissionRequirement : IAuthorizationRequirement + { + public SystemPermission[] RequiredPermissions; + + public SystemPermissionRequirement(SystemPermission[] requiredPermissions) + { + RequiredPermissions = requiredPermissions; + } + } + + public class SystemPermissionsHandler : AuthorizationHandler, IAuthorizationHandler + { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SystemPermissionRequirement requirement) + { + if (context.User == null) + { + context.Fail(); + } + else if (requirement.RequiredPermissions == null || requirement.RequiredPermissions.Length == 0) + { + context.Succeed(requirement); + } + else if (requirement.RequiredPermissions.Any(p => context.User.HasClaim(AuthorizationConstants.PermissionsClaimType, p.ToString()))) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Caster.Api/Infrastructure/Extensions/AuthorizationPolicyExtension.cs b/src/Caster.Api/Infrastructure/Extensions/AuthorizationPolicyExtension.cs index 799567e..b684446 100644 --- a/src/Caster.Api/Infrastructure/Extensions/AuthorizationPolicyExtension.cs +++ b/src/Caster.Api/Infrastructure/Extensions/AuthorizationPolicyExtension.cs @@ -5,6 +5,7 @@ using Caster.Api.Infrastructure.Authorization; using System; using Microsoft.AspNetCore.Authorization; +using Caster.Api.Domain.Models; namespace Caster.Api.Infrastructure.Extensions { @@ -19,19 +20,10 @@ public static void AddAuthorizationPolicy(this IServiceCollection services, Opti Array.ForEach(authOptions.AuthorizationScope.Split(' '), x => policyBuilder.RequireClaim("scope", x)); options.DefaultPolicy = policyBuilder.Build(); - - options.AddPolicy(nameof(CasterClaimTypes.SystemAdmin), policy => policy.Requirements.Add(new FullRightsRequirement())); - options.AddPolicy(nameof(CasterClaimTypes.ContentDeveloper), policy => policy.Requirements.Add(new ContentDeveloperRequirement())); - options.AddPolicy(nameof(CasterClaimTypes.BaseUser), policy => policy.Requirements.Add(new BaseUserRequirement())); - options.AddPolicy(nameof(CasterClaimTypes.Operator), policy => policy.Requirements.Add(new OperatorRequirement())); }); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } - - } } diff --git a/src/Caster.Api/Infrastructure/Extensions/DatabaseExtensions.cs b/src/Caster.Api/Infrastructure/Extensions/DatabaseExtensions.cs index 0056dd1..1aab585 100644 --- a/src/Caster.Api/Infrastructure/Extensions/DatabaseExtensions.cs +++ b/src/Caster.Api/Infrastructure/Extensions/DatabaseExtensions.cs @@ -45,15 +45,15 @@ public static IWebHost InitializeDatabase(this IWebHost webHost) private static void ProcessSeedDataOptions(SeedDataOptions options, CasterContext context) { - if (options.Permissions?.Any() == true) + if (options.Roles?.Any() == true) { - var dbPermissions = context.Permissions.ToList(); + var dbRoles = context.SystemRoles.ToHashSet(); - foreach (Permission permission in options.Permissions) + foreach (var role in options.Roles) { - if (!dbPermissions.Where(x => x.Key == permission.Key && x.Value == permission.Value).Any()) + if (!dbRoles.Any(x => x.Name == role.Name)) { - context.Permissions.Add(permission); + context.SystemRoles.Add(role); } } @@ -62,12 +62,27 @@ private static void ProcessSeedDataOptions(SeedDataOptions options, CasterContex if (options.Users?.Any() == true) { - var dbUsers = context.Users.ToList(); + var dbUserIds = context.Users.Select(x => x.Id).ToHashSet(); foreach (User user in options.Users) { - if (!dbUsers.Where(x => x.Id == user.Id).Any()) + if (!dbUserIds.Contains(user.Id)) { + if (user.Role?.Id == Guid.Empty && !string.IsNullOrEmpty(user.Role.Name)) + { + var role = context.SystemRoles.FirstOrDefault(x => x.Name == user.Role.Name); + if (role != null) + { + user.RoleId = role.Id; + user.Role = role; + } + else + { + user.RoleId = null; + user.Role = null; + } + } + context.Users.Add(user); } } @@ -75,15 +90,15 @@ private static void ProcessSeedDataOptions(SeedDataOptions options, CasterContex context.SaveChanges(); } - if (options.UserPermissions?.Any() == true) + if (options.Groups?.Any() == true) { - var dbUserPermissions = context.UserPermissions.ToList(); + var dbGroup = context.Groups.ToHashSet(); - foreach (UserPermission userPermission in options.UserPermissions) + foreach (var group in options.Groups) { - if (!dbUserPermissions.Where(x => x.UserId == userPermission.UserId && x.PermissionId == userPermission.PermissionId).Any()) + if (!dbGroup.Any(x => x.Name == group.Name)) { - context.UserPermissions.Add(userPermission); + context.Groups.Add(group); } } diff --git a/src/Caster.Api/Infrastructure/Extensions/ValidationExtensions.cs b/src/Caster.Api/Infrastructure/Extensions/ValidationExtensions.cs index 2a4a568..dd05fc0 100644 --- a/src/Caster.Api/Infrastructure/Extensions/ValidationExtensions.cs +++ b/src/Caster.Api/Infrastructure/Extensions/ValidationExtensions.cs @@ -66,4 +66,39 @@ public static IRuleBuilderOptions WorkspaceExists(this IRuleBuilder< .MustAsync(async (id, cancellationToken) => await validationService.WorkspaceExists(id)) .WithMessage("Workspace does not exist"); } + + public static IRuleBuilderOptions UserExists(this IRuleBuilder ruleBuilder, IValidationService validationService) + { + return ruleBuilder + .MustAsync(async (id, cancellationToken) => await validationService.UserExists(id)) + .WithMessage("User does not exist"); + } + + public static IRuleBuilderOptions GroupExists(this IRuleBuilder ruleBuilder, IValidationService validationService) + { + return ruleBuilder + .MustAsync(async (id, cancellationToken) => await validationService.GroupExists(id)) + .WithMessage("User does not exist"); + } + + public static IRuleBuilderOptions ProjectRoleExists(this IRuleBuilder ruleBuilder, IValidationService validationService) + { + return ruleBuilder + .MustAsync(async (id, cancellationToken) => await validationService.ProjectRoleExists(id)) + .WithMessage("Role does not exist"); + } + + public static IRuleBuilderOptions SystemRoleExists(this IRuleBuilder ruleBuilder, IValidationService validationService) + { + return ruleBuilder + .MustAsync(async (id, cancellationToken) => await validationService.SystemRoleExists(id)) + .WithMessage("Role does not exist"); + } + + public static IRuleBuilderOptions RunExists(this IRuleBuilder ruleBuilder, IValidationService validationService) + { + return ruleBuilder + .MustAsync(async (id, cancellationToken) => await validationService.RunExists(id)) + .WithMessage("Run does not exist"); + } } \ No newline at end of file diff --git a/src/Caster.Api/Infrastructure/Identity/IdentityResolver.cs b/src/Caster.Api/Infrastructure/Identity/IdentityResolver.cs index eca8843..e5cd889 100644 --- a/src/Caster.Api/Infrastructure/Identity/IdentityResolver.cs +++ b/src/Caster.Api/Infrastructure/Identity/IdentityResolver.cs @@ -1,9 +1,11 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +using System; using System.Security.Claims; using System.Threading.Tasks; using Caster.Api.Infrastructure.Authorization; +using Caster.Api.Infrastructure.Extensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -12,10 +14,11 @@ namespace Caster.Api.Infrastructure.Identity public interface IIdentityResolver { ClaimsPrincipal GetClaimsPrincipal(); + Guid GetId(); Task IsAdminAsync(); } - public class IdentityResolver: IIdentityResolver + public class IdentityResolver : IIdentityResolver { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAuthorizationService _authorizationService; @@ -30,7 +33,12 @@ public IdentityResolver( public ClaimsPrincipal GetClaimsPrincipal() { - return _httpContextAccessor?.HttpContext?.User as ClaimsPrincipal; + return _httpContextAccessor?.HttpContext?.User; + } + + public Guid GetId() + { + return this.GetClaimsPrincipal().GetId(); } public async Task IsAdminAsync() diff --git a/src/Caster.Api/Infrastructure/Options/ClaimsTransformationOptions.cs b/src/Caster.Api/Infrastructure/Options/ClaimsTransformationOptions.cs index 5cbd425..2f847c3 100644 --- a/src/Caster.Api/Infrastructure/Options/ClaimsTransformationOptions.cs +++ b/src/Caster.Api/Infrastructure/Options/ClaimsTransformationOptions.cs @@ -1,17 +1,16 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - namespace Caster.Api.Infrastructure.Options { public class ClaimsTransformationOptions { public bool EnableCaching { get; set; } public double CacheExpirationSeconds { get; set; } + public bool UseRolesFromIdP { get; set; } + public string RolesClaimPath { get; set; } + public bool UseGroupsFromIdP { get; set; } + public string GroupsClaimPath { get; set; } } } diff --git a/src/Caster.Api/Infrastructure/Options/SeedDataOptions.cs b/src/Caster.Api/Infrastructure/Options/SeedDataOptions.cs index f87ed77..bb1957e 100644 --- a/src/Caster.Api/Infrastructure/Options/SeedDataOptions.cs +++ b/src/Caster.Api/Infrastructure/Options/SeedDataOptions.cs @@ -8,9 +8,9 @@ namespace Caster.Api.Infrastructure.Options { public class SeedDataOptions { - public List Permissions { get; set; } + public List Roles { get; set; } public List Users { get; set; } - public List UserPermissions { get; set; } + public List Groups { get; set; } } } diff --git a/src/Caster.Api/Startup.cs b/src/Caster.Api/Startup.cs index cb1aef2..1985199 100644 --- a/src/Caster.Api/Startup.cs +++ b/src/Caster.Api/Startup.cs @@ -14,6 +14,7 @@ using Caster.Api.Features.Shared.Behaviors; using Caster.Api.Features.Shared.Services; using Caster.Api.Hubs; +using Caster.Api.Infrastructure.Authorization; using Caster.Api.Infrastructure.ClaimsTransformers; using Caster.Api.Infrastructure.DbInterceptors; using Caster.Api.Infrastructure.Exceptions.Middleware; @@ -218,9 +219,9 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(typeof(IDependencyAggregate<>), typeof(DependencyAggregate<>)); + services.AddScoped(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Caster.Api/appsettings.json b/src/Caster.Api/appsettings.json index a251c1b..0496b76 100644 --- a/src/Caster.Api/appsettings.json +++ b/src/Caster.Api/appsettings.json @@ -23,7 +23,11 @@ }, "ClaimsTransformation": { "EnableCaching": true, - "CacheExpirationSeconds": 60 + "CacheExpirationSeconds": 60, + "UseRolesFromIdP": true, + "RolesClaimPath": "realm_access.roles", + "UseGroupsFromIdP": true, + "GroupsClaimPath": "groups" }, "Client": { "TokenUrl": "http://localhost:5000/connect/token", @@ -68,33 +72,36 @@ "DaysToSaveDailyUntaggedVersions": 31 }, "SeedData": { - "Permissions": [ - { - "Id": "00000000-0000-0000-0000-000000000001", - "Key": "SystemAdmin", - "Value": "true", - "Description": "Has Full Rights. Can do everything.", - "ReadOnly": true - }, - { - "Id": "00000000-0000-0000-0000-000000000002", - "Key": "ContentDeveloper", - "Value": "true", - "Description": "Can create/edit/delete an Project/Directory/Workspace/File/Module", - "ReadOnly": true - } + "Roles": [ + // { + // "name": "Rangetech Admin", + // "allPermissions": false, + // "permissions": [ + // "CreateProjects", + // "ViewProjects", + // "EditProjects", + // "ManageProjects", + // "ImportProjects", + // "LockFiles" + // ] + // } ], "Users": [ - /* { - "id": "", - "name": "" - } */ + // { + // "id": "7493f145-dbcd-4ba8-9020-3aeba55bc2a1", + // "name": "Admin", + // "role": { + // "name": "Administrator" + // } + // } ], - "UserPermissions": [ - /* { - "UserId": "", - "PermissionId": "00000000-0000-0000-0000-000000000001" - } */ + "Groups": [ + // { + // "name": "Rangetechs" + // }, + // { + // "name": "White Cell" + // } ] } } From 6e7c882a43d4565adec184732bbabd72c734aec8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 19:15:05 +0000 Subject: [PATCH 2/2] Add missing document markings --- .../20241220192002_Granular_Permissions.Designer.cs | 5 +++++ .../Data/Migrations/20241220192002_Granular_Permissions.cs | 5 +++++ .../20241220192404_Migrate_Old_Permissions.Designer.cs | 5 +++++ .../Migrations/20241220192404_Migrate_Old_Permissions.cs | 5 +++++ .../20241220200146_Remove_Old_Permissions.Designer.cs | 5 +++++ .../Data/Migrations/20241220200146_Remove_Old_Permissions.cs | 5 +++++ .../Migrations/20241220202800_Unique_Group_Names.Designer.cs | 5 +++++ .../Data/Migrations/20241220202800_Unique_Group_Names.cs | 5 +++++ .../20250102200043_Add_Import_Resources.Designer.cs | 5 +++++ .../Data/Migrations/20250102200043_Add_Import_Resources.cs | 5 +++++ src/Caster.Api/Data/Migrations/CasterContextModelSnapshot.cs | 5 +++++ src/Caster.Api/Domain/Models/IEntity.cs | 5 +++++ .../Infrastructure/Authorization/AuthorizationConstants.cs | 5 +++++ .../Infrastructure/Authorization/AuthorizationService.cs | 5 +++++ .../Infrastructure/Authorization/ProjectPermissionClaim.cs | 5 +++++ 15 files changed, 75 insertions(+) diff --git a/src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.Designer.cs b/src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.Designer.cs index 0af5680..f972715 100644 --- a/src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.Designer.cs +++ b/src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.Designer.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + // using System; using Caster.Api.Data; diff --git a/src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.cs b/src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.cs index 3c47fe7..592a4ef 100644 --- a/src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.cs +++ b/src/Caster.Api/Data/Migrations/20241220192002_Granular_Permissions.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + using System; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.Designer.cs b/src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.Designer.cs index 841d268..202c309 100644 --- a/src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.Designer.cs +++ b/src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.Designer.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + // using System; using Caster.Api.Data; diff --git a/src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.cs b/src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.cs index 986b683..d3d2014 100644 --- a/src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.cs +++ b/src/Caster.Api/Data/Migrations/20241220192404_Migrate_Old_Permissions.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.Designer.cs b/src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.Designer.cs index 4a7a0c2..9516984 100644 --- a/src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.Designer.cs +++ b/src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.Designer.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + // using System; using Caster.Api.Data; diff --git a/src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.cs b/src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.cs index 77f227c..fca4be5 100644 --- a/src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.cs +++ b/src/Caster.Api/Data/Migrations/20241220200146_Remove_Old_Permissions.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + using System; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.Designer.cs b/src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.Designer.cs index ea7c3a3..0c9ea49 100644 --- a/src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.Designer.cs +++ b/src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.Designer.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + // using System; using Caster.Api.Data; diff --git a/src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.cs b/src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.cs index 88ef018..b21e7ef 100644 --- a/src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.cs +++ b/src/Caster.Api/Data/Migrations/20241220202800_Unique_Group_Names.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.Designer.cs b/src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.Designer.cs index e4befeb..538ae26 100644 --- a/src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.Designer.cs +++ b/src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.Designer.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + // using System; using Caster.Api.Data; diff --git a/src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.cs b/src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.cs index a7cd574..f9caed7 100644 --- a/src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.cs +++ b/src/Caster.Api/Data/Migrations/20250102200043_Add_Import_Resources.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + using System; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Caster.Api/Data/Migrations/CasterContextModelSnapshot.cs b/src/Caster.Api/Data/Migrations/CasterContextModelSnapshot.cs index 91f1715..6975313 100644 --- a/src/Caster.Api/Data/Migrations/CasterContextModelSnapshot.cs +++ b/src/Caster.Api/Data/Migrations/CasterContextModelSnapshot.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + // using System; using Caster.Api.Data; diff --git a/src/Caster.Api/Domain/Models/IEntity.cs b/src/Caster.Api/Domain/Models/IEntity.cs index cde34de..eb1a519 100644 --- a/src/Caster.Api/Domain/Models/IEntity.cs +++ b/src/Caster.Api/Domain/Models/IEntity.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + namespace Caster.Api.Domain.Models; public interface IEntity { } \ No newline at end of file diff --git a/src/Caster.Api/Infrastructure/Authorization/AuthorizationConstants.cs b/src/Caster.Api/Infrastructure/Authorization/AuthorizationConstants.cs index a88ecea..761e856 100644 --- a/src/Caster.Api/Infrastructure/Authorization/AuthorizationConstants.cs +++ b/src/Caster.Api/Infrastructure/Authorization/AuthorizationConstants.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + using System; using Caster.Api.Domain.Models; diff --git a/src/Caster.Api/Infrastructure/Authorization/AuthorizationService.cs b/src/Caster.Api/Infrastructure/Authorization/AuthorizationService.cs index bf53b56..40064d3 100644 --- a/src/Caster.Api/Infrastructure/Authorization/AuthorizationService.cs +++ b/src/Caster.Api/Infrastructure/Authorization/AuthorizationService.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + using System; using System.Collections.Generic; using System.Linq; diff --git a/src/Caster.Api/Infrastructure/Authorization/ProjectPermissionClaim.cs b/src/Caster.Api/Infrastructure/Authorization/ProjectPermissionClaim.cs index 9c8f7e5..b1ac3f0 100644 --- a/src/Caster.Api/Infrastructure/Authorization/ProjectPermissionClaim.cs +++ b/src/Caster.Api/Infrastructure/Authorization/ProjectPermissionClaim.cs @@ -1,3 +1,8 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + using System; using System.Text.Json; using Caster.Api.Domain.Models;