import { GraphqlClient } from './../common/GraphqlClient'; import { ManagementTokenProvider } from './ManagementTokenProvider'; import { IAppAccessPolicy, IAppAccessPolicyQueryFilter, IApplication, IApplicationAccessPolicies, IResourceDto, IResourceQueryFilter, IResourceResponse, IResourceUpdateDto, ManagementClientOptions, Namespace, Namespaces, ProgrammaticAccessAccount, ProgrammaticAccessAccountList } from './types'; import { allow, authorizeResource, listAuthorizedResources, authorizedTargets } from '../graphqlapi'; import { AuthorizeResourceOpt, CommonMessage, PaginatedAuthorizedResources, PolicyAssignmentTargetType, ResourceType } from '../../types/graphql.v2'; import { formatAuthorizedResources, serialize } from '../utils'; import { HttpClient } from '../common/HttpClient'; import { AuthorizeResourcesParams, ListAuthorizedResourcesBatchParams, ListAuthorizedResourcesBatchResult, RevokeResourcesParams } from './AclManagementClient.interface'; /** * @class AclManagementClient 管理权限、访问控制 * @description Authing 基于 PBAC(Policy Based Access Control,基于策略的访问控制)构建权限模型, * 可以和 RBAC (Role Based Access Control,基于角色的访问控制)结合,实现非常灵活、精细化的权限控制。 * 此模块将此模型抽象成了两个方法: allow, isAllowed。 * * @example * * 请使用以下方式使用该模块,而不要直接初始化该模块: * * \`\`\`javascript * import { ManagementClient } from "authing-js-sdk" * const managementClient = new ManagementClient({ * userPoolId: "YOUR_USERPOOL_ID", * secret: "YOUR_USERPOOL_SECRET", * }) * managementClient.acl.allow // 允许某个用户对某个资源进行某个操作 * managementClient.acl.isAllowed // 判断某个用户是否对某个资源有某个操作权限 * \`\`\` * * @name AclManagementClient */ export class AclManagementClient { options: ManagementClientOptions; graphqlClient: GraphqlClient; httpClient: HttpClient; tokenProvider: ManagementTokenProvider; constructor( options: ManagementClientOptions, graphqlClient: GraphqlClient, httpClient: HttpClient, tokenProvider: ManagementTokenProvider ) { this.options = options; this.graphqlClient = graphqlClient; this.httpClient = httpClient; this.tokenProvider = tokenProvider; } /** * 生成随机字符串 * @param randomLenth 随机长度 * @returns string */ public static randomString(randomLenth: number = 32): string { randomLenth = randomLenth || 32; const t = 'abcdefhijkmnprstwxyz2345678'; const a = t.length; let n = ''; for (let i = 0; i < randomLenth; i++) { n += t.charAt(Math.floor(Math.random() * a)); } return n; } /** * @name allow * @name_zh 允许某个用户对某个资源进行某个操作 * * @description 允许某个用户对某个资源进行某个操作 * * @param {string} userId 用户 ID * @param {string} action 操作名称,推荐使用 \:\ 的格式,如 `books:edit`, `books:list` * @param {string} resource 资源名称, 必须为 \:\ 格式或者为 *, 如 `*`, `books:123`, `books:*` * @example * managementClient.acl.allow('USERID1', 'books:123', 'books:read') * managementClient.acl.isAllowed('USERID1', 'books:123', 'books:read') // true * managementClient.acl.isAllowed('USERID1', 'books:123', 'books:edit') // false * * @example * managementClient.acl.allow('USERID2', 'books:*', 'books:*') * managementClient.acl.isAllowed('USERID2', 'books:123', 'books:read') // true * managementClient.acl.isAllowed('USERID2', 'books:124', 'books:edit') // true * * @returns {Promise} * @memberof AclManagementClient */ async allow( userId: string, resource: string, action: string, namespace: string ): Promise { const { allow: data } = await allow( this.graphqlClient, this.tokenProvider, { resource, action, userId, namespace } ); return data; } /** * @name isAllowed * @name_zh 判断某个用户是否对某个资源有某个操作权限 * * @description 判断某个用户是否对某个资源有某个操作权限 * * @param {string} userId 用户ID * @param {string} action 操作名称,推荐使用 \:\ 的格式,如 `books:edit`, `books:list` * @param {string} resource 资源名称, 必须为 \:\ 格式或者为 *, 如 `*`, `books:123`, `books:*` * @example * managementClient.acl.isAllowed('USERID', 'books:*', 'books:edit') * * @returns {Promise} 是否具备操作权限 * @memberof AclManagementClient * */ public async isAllowed( userId: string, resource: string, action: string, opts?: { namespace?: string; tenantId?: string; } ): Promise { const { namespace, tenantId } = opts || {}; const { allowed } = await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/acl/is-allowed`, data: { namespace, userId, resource, action, tenantId }, headers: { [this.options.headers['tenant-id']]: tenantId } }); return allowed; } /** * @description 获取用户被授权的所有资源 * * @param userId * @param namespace */ public async listAuthorizedResources( targetType: PolicyAssignmentTargetType, targetIdentifier: string, namespace: string, options?: { resourceType?: ResourceType; } ): Promise { const { resourceType } = options || {}; let { authorizedResources: { list, totalCount } } = await listAuthorizedResources(this.graphqlClient, this.tokenProvider, { targetType, targetIdentifier, namespace, resourceType }); list = formatAuthorizedResources(list); return { list, totalCount }; } public async listAuthorizedResourcesBatch( params: ListAuthorizedResourcesBatchParams ): Promise { const { namespace, tenantId, targets, resourceType } = params; const data = await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/acl/list-authorized-resources-batch`, data: { namespace, resourceType, targets }, headers: { [this.options.headers['tenant-id']]: tenantId } }); return data; } /** * @description 将一个(类)资源授权给用户、角色、分组、组织机构,且可以分别指定不同的操作权限。 * */ public async authorizeResource(params: { namespace: string; resource: string; opts: AuthorizeResourceOpt[]; }): Promise { const { namespace, resource, opts } = params; const { authorizeResource: data } = await authorizeResource( this.graphqlClient, this.tokenProvider, { namespace, resource, opts } ); return data; } /** * @description 批量授权资源 * */ public async authorizeResources(params: AuthorizeResourcesParams) { const { namespace, tenantId, opts } = params; await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/acl/authorize-resources`, data: { namespace, opts }, headers: { [this.options.headers['tenant-id']]: tenantId } }); return true; } /** * @description 批量撤销资源 */ public async revokeResources(params: RevokeResourcesParams) { const { namespace, tenantId, opts } = params; await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/acl/revoke-resources`, data: { namespace, opts }, headers: { [this.options.headers['tenant-id']]: tenantId } }); return true; } public async revokeResource(params: { namespace: string; resource: string; opts: { targetType: PolicyAssignmentTargetType; targetIdentifier: string; }[]; }) { const { namespace, resource, opts } = params; await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/acl/revoke-resource`, data: { namespace, resource, opts } }); return true; } /** * @description 获取具备某个(类)资源操作权限的用户、分组、角色、组织机构。 * @param namespace {string} 权限分组标识 * @param resource {string} 资源标识 * @param actions {string[]} 资源操作标识 * @param targetType {string} 筛选项,指定返回主体的类型,可选值为 'USER'、'ROLE'、'ORG'、'GROUP' */ public async getAuthorizedTargets(options: { namespace: string; resource: string; resourceType: 'BUTTON' | 'UI' | 'MENU' | 'API' | 'DATA'; actions?: { op: 'AND' | 'OR'; list: string[]; }; targetType?: 'USER' | 'ROLE' | 'ORG' | 'GROUP'; }) { if (!options) { throw new Error( '请传入 options.namespace、options.resource、options.actions,含义为权限分组标识、资源标识、资源操作标识' ); } if (!options.namespace) { throw new Error('请传入 options.namespace,含义为权限分组标识'); } if (!options.resource) { throw new Error('请传入 options.resource,含义为资源标识'); } if (!options.resourceType) { throw new Error('请传入 options.resourceType,含义为资源类型'); } const { authorizedTargets: data } = await authorizedTargets( this.graphqlClient, this.tokenProvider, { namespace: options.namespace, resourceType: options.resourceType as any, resource: options.resource, targetType: options.targetType as any, actions: options.actions as any } ); return data; } public async listResources(options?: IResourceQueryFilter) { return await this.httpClient.request({ method: 'GET', url: `${this.options.host}/api/v2/resources`, params: { namespace: options?.namespace || options?.namespaceCode, type: options?.type, limit: options?.limit || 10, page: options?.page || 1 } }); } public async getResourceById(id: string) { const data = await this.httpClient.request({ method: 'GET', url: `${this.options.host}/api/v2/resources/detail`, params: { id } }); if (!data) { return null; } return data; } public async getResourceByCode(options: { namespace: string; code: string }) { const { namespace, code } = options; const data = await this.httpClient.request({ method: 'GET', url: `${this.options.host}/api/v2/resources/detail`, params: { namespace, code } }); if (!data) { return null; } return data; } /** * @deprecated use listResources * @param options */ public async getResources(options?: IResourceQueryFilter) { return await this.listResources(options); } public async createResource( options: IResourceDto ): Promise { if (!options) { throw new Error('请传入资源数据'); } if (!options.code) { throw new Error('请为资源设定一个资源标识符'); } if (!options.actions || options?.actions.length === 0) { throw new Error('请至少定义一个资源操作'); } if (!options.namespace) { throw new Error('请传入权限分组标识符'); } return await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/resources`, data: options }); } public async updateResource( code: string, options: IResourceUpdateDto ): Promise { if (!code) { throw new Error('请传入资源标识符'); } return await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/resources/${code}`, data: options }); } public async deleteResource( code: string, namespace: string ): Promise { if (!code) { throw new Error('请传入资源标识符'); } if (!namespace) { throw new Error('请传入权限分组标识符'); } await this.httpClient.request({ method: 'DELETE', url: `${this.options.host}/api/v2/resources/${code}`, params: { namespace } }); return true; } public async getApplicationAccessPolicies( options: IAppAccessPolicyQueryFilter ): Promise { if (!options?.appId) { throw new Error('请传入 appId'); } const { appId, page, limit } = options; return await this.httpClient.request({ method: 'GET', url: `${this.options.host}/api/v2/applications/${appId}/authorization/records`, params: { page, limit } }); } public async enableApplicationAccessPolicy(options: IAppAccessPolicy) { if (!options?.appId) { throw new Error('请传入 appId'); } if (!options?.targetType) { throw new Error( '请传入主体类型,可选值为 USER、ROLE、ORG、GROUP,含义为用户、角色、组织机构节点、用户分组' ); } if (!options?.targetIdentifiers) { throw new Error('请传入主体 id'); } const { namespace, targetIdentifiers, targetType, appId, inheritByChildren } = options; await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/applications/${appId}/authorization/enable-effect`, data: { targetType, namespace, targetIdentifiers, inheritByChildren } }); return { code: 200, message: '启用应用访问控制策略成功' }; } public async disableApplicationAccessPolicy(options: IAppAccessPolicy) { if (!options?.appId) { throw new Error('请传入 appId'); } if (!options?.targetType) { throw new Error( '请传入主体类型,可选值为 USER、ROLE、ORG、GROUP,含义为用户、角色、组织机构节点、用户分组' ); } if (!options?.targetIdentifiers) { throw new Error('请传入主体 id'); } const { namespace, targetIdentifiers, targetType, appId, inheritByChildren } = options; await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/applications/${appId}/authorization/disable-effect`, data: { targetType, namespace, targetIdentifiers, inheritByChildren } }); return { code: 200, message: '停用应用访问控制策略成功' }; } public async deleteApplicationAccessPolicy(options: IAppAccessPolicy) { if (!options?.appId) { throw new Error('请传入 appId'); } if (!options?.targetType) { throw new Error( '请传入主体类型,可选值为 USER、ROLE、ORG、GROUP,含义为用户、角色、组织机构节点、用户分组' ); } if (!options?.targetIdentifiers) { throw new Error('请传入主体 id'); } const { namespace, targetIdentifiers, targetType, appId, inheritByChildren } = options; await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/applications/${appId}/authorization/revoke`, data: { targetType, namespace, targetIdentifiers, inheritByChildren } }); return { code: 200, message: '删除应用访问控制策略成功' }; } public async allowAccessApplication(options: IAppAccessPolicy) { if (!options?.appId) { throw new Error('请传入 appId'); } if (!options?.targetType) { throw new Error( '请传入主体类型,可选值为 USER、ROLE、ORG、GROUP,含义为用户、角色、组织机构节点、用户分组' ); } if (!options?.targetIdentifiers) { throw new Error('请传入主体 id'); } const { namespace, targetIdentifiers, targetType, appId, inheritByChildren } = options; await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/applications/${appId}/authorization/allow`, data: { targetType, namespace, targetIdentifiers, inheritByChildren } }); return { code: 200, message: '允许主体访问应用的策略配置已生效' }; } public async denyAccessApplication(options: IAppAccessPolicy) { if (!options?.appId) { throw new Error('请传入 appId'); } if (!options?.targetType) { throw new Error( '请传入主体类型,可选值为 USER、ROLE、ORG、GROUP,含义为用户、角色、组织机构节点、用户分组' ); } if (!options?.targetIdentifiers) { throw new Error('请传入主体 id'); } const { namespace, targetIdentifiers, targetType, appId, inheritByChildren } = options; await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/applications/${appId}/authorization/deny`, data: { targetType, namespace, targetIdentifiers, inheritByChildren } }); return { code: 200, message: '拒绝主体访问应用的策略配置已生效' }; } public async updateDefaultApplicationAccessPolicy(options: { defaultStrategy: 'ALLOW_ALL' | 'DENY_ALL'; appId: string; }): Promise { if (!options?.appId) { throw new Error('请传入应用 id'); } if (!options?.defaultStrategy) { throw new Error( '请传入默认策略,可选值为 ALLOW_ALL、DENY_ALL,含义为默认允许所有用户登录应用、默认拒绝所有用户登录应用' ); } return await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/applications/${options.appId}`, data: { permissionStrategy: { defaultStrategy: options.defaultStrategy } } }); } /** * 编程访问账号列表 * @param appId 应用 ID * @param page 当前页数 * @param limit 每页显示条数 * @returns Promise */ public async programmaticAccessAccountList( appId: string, page: number = 1, limit: number = 10 ): Promise { const result = await this.httpClient.request({ method: 'GET', url: `${this.options.host}/api/v2/applications/${appId}/programmatic-access-accounts?limit=${limit}&page=${page}` }); return result; } /** * 添加编程访问账号 * @param appId 应用 ID * @param options.tokenLifetime AccessToken 过期时间(秒) * @param options.remarks 备注 * @returns Promise */ public async createProgrammaticAccessAccount( appId: string, options: { tokenLifetime: number; remarks?: string; } = { tokenLifetime: 600 } ): Promise { const result = await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/applications/${appId}/programmatic-access-accounts`, data: { ...options } }); return result; } /** * 添加编程访问账号 * @param programmaticAccessAccountId 编程访问账号 ID * @returns Promise */ public async deleteProgrammaticAccessAccount( programmaticAccessAccountId: string ): Promise { try { await this.httpClient.request({ method: 'DELETE', url: `${this.options.host}/api/v2/applications/programmatic-access-accounts?id=${programmaticAccessAccountId}` }); return true; } catch (error) { throw error; } } /** * 刷新编程访问账号密钥 * @param programmaticAccessAccountId 编程访问账号 ID * @param programmaticAccessAccountSecret 编程访问账号 Secret * @returns Promise */ public async refreshProgrammaticAccessAccountSecret( programmaticAccessAccountId: string, programmaticAccessAccountSecret: string = AclManagementClient.randomString( 32 ) ): Promise { const result = await this.httpClient.request({ method: 'PATCH', url: `${this.options.host}/api/v2/applications/programmatic-access-accounts`, data: { id: programmaticAccessAccountId, secret: programmaticAccessAccountSecret } }); return result; } /** * 启用编程访问账号 * @param programmaticAccessAccountId 编程访问账号 ID * @returns Promise */ public async enableProgrammaticAccessAccount( programmaticAccessAccountId: string ): Promise { const result = await this.httpClient.request({ method: 'PATCH', url: `${this.options.host}/api/v2/applications/programmatic-access-accounts`, data: { id: programmaticAccessAccountId, enabled: true } }); return result; } /** * 禁用编程访问账号 * @param programmaticAccessAccountId 编程访问账号 ID * @returns Promise */ public async disableProgrammaticAccessAccount( programmaticAccessAccountId: string ): Promise { const result = await this.httpClient.request({ method: 'PATCH', url: `${this.options.host}/api/v2/applications/programmatic-access-accounts`, data: { id: programmaticAccessAccountId, enabled: false } }); return result; } /** * 权限分组列表 * @param page 当前页数 * @param limit 每页显示条数 * @returns Promise */ public async listNamespaces( page: number = 1, limit: number = 10 ): Promise { const result = await this.httpClient.request({ method: 'GET', url: `${this.options.host}/api/v2/resource-namespace/${this.options.userPoolId}?` + serialize({ page: page?.toString(), limit: limit?.toString() }) }); return result; } /** * 删除权限分组 * @param code 权限分组 Code * @returns Promise */ public async deleteNamespace(code: string): Promise { try { await this.httpClient.request({ method: 'DELETE', url: `${this.options.host}/api/v2/resource-namespace/${this.options.userPoolId}/code/${code}` }); return true; } catch (error) { throw error; } } /** * 创建权限分组 * @param code 权限分组 Code * @param name 权限分组名称 * @param description 权限分组描述 * @returns Promise */ public async createNamespace( code: string, name: string, description?: string ): Promise { try { const data = await this.httpClient.request({ method: 'POST', url: `${this.options.host}/api/v2/resource-namespace/${this.options.userPoolId}`, data: { name, code, description } }); return data; } catch (error) { throw error; } } /** * 修改权限分组 * @param code 权限分组 Code * @param name 权限分组名称 * @param code 权限分组 Code * @param description 权限分组描述 * @returns Promise */ public async updateNamespace( code: string, updates: { name?: string; code?: string; description?: string; } ): Promise { try { const data = await this.httpClient.request({ method: 'PUT', url: `${this.options.host}/api/v2/resource-namespace/${this.options.userPoolId}/code/${code}`, data: { name: updates.name, code: updates.code, description: updates.description } }); return data; } catch (error) { throw error; } } //授权详情 -获取下面的规则信息 public async getPoliciesAssignmentsCondition( namespace: string, policyCode:string, targetType:string ): Promise { const result = await this.httpClient.request({ method: 'GET', url: `${this.options.host}/api/v2/policies/assignments/by-target?namespace=${namespace}&policy_code=${policyCode}&target_type=${targetType}` }); return result; } }