diff --git a/config/errors.php b/config/errors.php new file mode 100644 index 00000000..5a5e3f5f --- /dev/null +++ b/config/errors.php @@ -0,0 +1,5 @@ + 202001, +]; diff --git a/src/Metadata/PermissionTrait.php b/src/Metadata/PermissionTrait.php new file mode 100644 index 00000000..5319b064 --- /dev/null +++ b/src/Metadata/PermissionTrait.php @@ -0,0 +1,22 @@ +getOption('requireAuth')) { + return $next(); + } + + $ret = User::cur()->checkPagePermission($this->req->getMethod(), $this->req->getPathInfo()); + if ($ret->isErr()) { + return $ret; + } + return $next(); + } +} diff --git a/src/Migration/V20220810105803CreatePermissionTables.php b/src/Migration/V20220810105803CreatePermissionTables.php new file mode 100644 index 00000000..33d7a731 --- /dev/null +++ b/src/Migration/V20220810105803CreatePermissionTables.php @@ -0,0 +1,83 @@ +schema->table('permissions')->tableComment('权限') + ->bigId()->comment('编号') + ->uBigInt('app_id')->comment('应用编号') + ->string('name', 32)->comment('名称') + ->string('code', 128)->comment('标识') + ->string('description')->comment('描述') + ->bool('is_enabled')->comment('是否启用')->defaults(true) + ->timestamps() + ->userstamps() + ->softDeletable() + ->exec(); + + $this->schema->table('roles')->tableComment('角色') + ->bigId()->comment('编号') + ->uBigInt('app_id')->comment('应用编号') + ->uBigInt('parent_id')->comment('父级角色编号') + ->uTinyInt('level')->comment('层级')->defaults(1) + ->string('name', 32)->comment('名称') + ->string('code', 128)->comment('标识') + ->string('description')->comment('描述') + ->bool('is_enabled')->comment('是否启用')->defaults(true) + ->json('actions')->comment('菜单和操作') + ->timestamps() + ->userstamps() + ->softDeletable() + ->exec(); + + $this->schema->table('permissions_roles')->tableComment('角色权限') + ->bigId()->comment('编号') + ->uBigInt('app_id')->comment('应用编号') + ->uBigInt('role_id')->comment('角色编号') + ->uBigInt('permission_id')->comment('权限编号') + ->timestamps() + ->userstamps() + ->softDeletable() + ->exec(); + + $this->schema->table('permissions_users')->tableComment('用户权限') + ->bigId() + ->uBigInt('app_id')->comment('应用编号') + ->uBigInt('user_id')->comment('用户编号') + ->uBigInt('permission_id')->comment('权限编号') + ->timestamps() + ->userstamps() + ->softDeletable() + ->exec(); + + $this->schema->table('roles_users')->tableComment('用户角色') + ->bigId() + ->uBigInt('app_id')->comment('应用编号') + ->uBigInt('user_id')->comment('用户编号') + ->uBigInt('role_id')->comment('角色编号') + ->timestamps() + ->userstamps() + ->softDeletable() + ->exec(); + } + + /** + * {@inheritdoc} + */ + public function down() + { + $this->schema->dropIfExists('permissions'); + $this->schema->dropIfExists('roles'); + $this->schema->dropIfExists('permissions_roles'); + $this->schema->dropIfExists('permissions_users'); + $this->schema->dropIfExists('roles_users'); + } +} diff --git a/src/Model/HasPermissionTrait.php b/src/Model/HasPermissionTrait.php new file mode 100644 index 00000000..279603b9 --- /dev/null +++ b/src/Model/HasPermissionTrait.php @@ -0,0 +1,142 @@ +getActionPermissionCodes(), + $this->enabledRoles->enabledPermissions->getAll('code'), + $this->enabledPermissions->getAll('code') + )); + } + + /** + * @return string[] + */ + public function getActionPermissionCodes(): array + { + if ($this->isSuperAdmin()) { + return ['*']; + } + + $actions = $this->enabledRoles->getAll('actions'); + return array_unique(array_merge(...$actions)); + } + + /** + * Check if user have the specified permission + * + * @param string $code + * @return Ret + */ + public function checkPermission(string $code): Ret + { + if ($this->hasPermission($code)) { + return suc(); + } + return err('很抱歉,您没有权限执行该操作'); + } + + /** + * Whether the user have the specified permission + * + * @param string $code + * @return bool + */ + public function hasPermission(string $code): bool + { + // TODO 根据场景实现逐级查找,变量查找按需查找 + return in_array($code, $this->getPermissionCodes(), true); + } + + /** + * @param string $method + * @param string $path + * @return Ret + */ + public function checkPagePermission(string $method, string $path): Ret + { + if ($this->hasPagePermission($method, $path)) { + return suc(); + } + return err('很抱歉,您没有权限执行该操作'); + } + + public function hasPagePermission(string $method, string $path): bool + { + if ($this->isSuperAdmin()) { + return true; + } + + // 1. 获取权限 + $permissions = $this->getActionPermissionCodes(); + $this->logger->debug('Get user menu permissions', $permissions); + + // 2. 转换菜单为页面 + $map = $this->permissionMap->getMap(); + $map = array_intersect_key($map, array_flip($permissions)); + $map = array_unique(array_merge(...array_values($map))); + $this->logger->debug('Get user action permissions', $map); + + // 3. 检查当前页面是否在里面 + $path = ltrim($path, '/'); + if ($this->hasPagePermissionIn($method, $path, $map)) { + return true; + } + + // Whether has role permission + $rolePermissionCodes = $this->enabledRoles->enabledPermissions->getAll('code'); + if ($this->hasPagePermissionIn($method, $path, $rolePermissionCodes)) { + return true; + } + + // Whether has direct permission + $permissionCodes = $this->enabledPermissions->getAll('code'); + return $this->hasPagePermissionIn($method, $path, $permissionCodes); + } + + /** + * Whether has page permission in the specified permission codes + * + * @param string $method + * @param string $path + * @param array $permissions + * @return bool + */ + protected function hasPagePermissionIn(string $method, string $path, array $permissions): bool + { + $path = ltrim($path, '/'); + foreach ($permissions as $permission) { + $parts = explode(' ', $permission, 2); + $apiMethod = $parts[0]; + $apiPath = $parts[1] ?? null; + + if ($method !== $apiMethod) { + continue; + } + + if ($apiPath === $path) { + return true; + } + + if (false !== strpos($apiPath, '[')) { + $regex = preg_replace('#[.\+*?[^\]${}=!|:-]#', '\\\\$0', $apiPath); + $regex = str_replace(['\[', '\]'], ['(?P<', '>.+?)'], $regex); + $regex = '#^' . $regex . '$#uUD'; + if (preg_match($regex, $path)) { + return true; + } + } + } + return false; + } +} diff --git a/src/Service/Permission.php b/src/Service/Permission.php new file mode 100644 index 00000000..5904ba05 --- /dev/null +++ b/src/Service/Permission.php @@ -0,0 +1,59 @@ +isEnabledCheck; + } + + /** + * Whether enabled role management + * + * @return bool + * @svc + */ + protected function isEnabledRoleManage(): bool + { + return $this->isEnabledRoleManage; + } + + /** + * Whether enabled permission management + * + * @return bool + * @svc + */ + protected function isEnabledPermissionManage(): bool + { + return $this->isEnabledPermissionManage; + } +} diff --git a/src/Service/PermissionMap.php b/src/Service/PermissionMap.php new file mode 100644 index 00000000..0c85a81f --- /dev/null +++ b/src/Service/PermissionMap.php @@ -0,0 +1,139 @@ +map) { + $this->event->trigger('permissionGetMap', $this); + } + return $this->map; + } + + /** + * @param string $prefix + * @param callable $callable + * @return $this + */ + public function prefix(string $prefix, callable $callable): self + { + $this->prefix = $prefix; + $callable($this); + $this->prefix = ''; + return $this; + } + + public function add($permission, $permissions): self + { + $permission = $this->addPrefix($permission); + $this->map[$permission] = (array) $permissions; + return $this; + } + + public function addList(string $basePath = '', array $additional = []): self + { + [$scope, $resource] = $this->parse($this->addPrefix($basePath)); + return $this->add($basePath, array_merge([ + $this->buildGet($scope, $resource), + ], $additional)); + } + + public function addNew(string $basePath = '', array $additional = []): self + { + [$scope, $resource] = $this->parse($this->addPrefix($basePath)); + return $this->add($basePath . '/new', array_merge([ + $this->buildGet($scope, $resource, 'defaults'), + $this->buildPost($scope, $resource), + ], $additional)); + } + + public function addEdit(string $basePath = '', array $additional = []): self + { + [$scope, $resource] = $this->parse($this->addPrefix($basePath)); + return $this->add($basePath . '/[id]/edit', array_merge([ + $this->buildGet($scope, $resource, '[id]'), + $this->buildPatch($scope, $resource, '[id]'), + ], $additional)); + } + + public function addDelete(string $basePath = '', array $additional = []): self + { + [$scope, $resource] = $this->parse($this->addPrefix($basePath)); + return $this->add($basePath . '/[id]/delete', array_merge([ + $this->buildDelete($scope, $resource, '[id]'), + ], $additional)); + } + + public function addShow(string $basePath = '', array $additional = []): self + { + [$scope, $resource] = $this->parse($this->addPrefix($basePath)); + return $this->add($basePath . '/[id]', array_merge([ + $this->buildGet($scope, $resource, '[id]'), + ], $additional)); + } + + protected function addPrefix(string $basePath): string + { + if ($this->prefix && $basePath) { + return $this->prefix . '/' . ltrim($basePath, '/'); + } else { + return $this->prefix . $basePath; + } + } + + protected function parse(string $permission): array + { + return explode('/', $permission, 3); + } + + protected function buildGet(string $scope, string $resource, string $more = null): string + { + return $this->build('GET', $scope, $resource, $more); + } + + protected function buildPost(string $scope, string $resource, string $more = null): string + { + return $this->build('POST', $scope, $resource, $more); + } + + protected function buildPut(string $scope, string $resource, string $more = null): string + { + return $this->build('PUT', $scope, $resource, $more); + } + + protected function buildPatch(string $scope, string $resource, string $more = null): string + { + return $this->build('PATCH', $scope, $resource, $more); + } + + protected function buildDelete(string $scope, string $resource, string $more = null): string + { + return $this->build('DELETE', $scope, $resource, $more); + } + + protected function build(string $method, string $scope, string $resource, string $more = null): string + { + return $method . ' api/' . ($scope ? ($scope . '/') : '') . $resource . ($more ? ('/' . $more) : ''); + } +} diff --git a/src/Service/PermissionModel.php b/src/Service/PermissionModel.php new file mode 100644 index 00000000..8e67527e --- /dev/null +++ b/src/Service/PermissionModel.php @@ -0,0 +1,21 @@ +belongsToMany(PermissionModel::class)->whereNull('permissions_roles.deleted_at'); + } + + public function enabledPermissions(): PermissionModel + { + return $this->permissions()->where('isEnabled', true); + } + + public function checkDestroy(): Ret + { + if (RolesUserModel::findBy('roleId', $this->id)) { + return err(['很抱歉,该%s已被%s使用,不能删除', '角色', '用户']); + } + return suc(); + } +} diff --git a/src/Service/RolesUserModel.php b/src/Service/RolesUserModel.php new file mode 100644 index 00000000..f5d9666b --- /dev/null +++ b/src/Service/RolesUserModel.php @@ -0,0 +1,19 @@ +belongsToMany(RoleModel::class)->whereNull('roles_users.deleted_at'); + } + + public function enabledRoles(): RoleModel + { + return $this->roles()->where('isEnabled', true); + } + + public function permissions(): PermissionModel + { + return $this->belongsToMany(PermissionModel::class)->whereNull('permissions_users.deleted_at'); + } + + public function enabledPermissions(): PermissionModel + { + return $this->permissions()->where('isEnabled', true); + } }