# 安全

# 导入

通过如下方式导入安全api

import security from "svr-api/security";

# 方法

/**
* 此文件定义一些登录、用户信息操作、信息验证登安全相关的全局函数。
*/

/**
 * 获取当前用户是否已经登录
 */
export function isLogin(): boolean;

/**
 * 获取当前请求的sessionid
 *
 * @return 如果session不存在,返回null
 */
export function getSessionId(): string;

/**
 * 退出登录
 *
 * @param userid 如果不传递userid,那么默认退出当前登录用户,如果传递了那么退出指定的用户,当退出指
 *        定的用户时,会确保集群中所有节点都退出指定的用户。
 */
export function logout(userid?: string): void;

/**
 * 获取当前登录用户的信息
 * 
 * @return 在未登录的情况下调用本方法会返回`null`
 */
export function getCurrentUser(): CurrentUser;

/**
 * 使用用户名和密码进行登录
 * 
 * @param userId 用户名
 * @param password 密码
 * @param userDirectory 用户类型,`sys`代表系统用户,`external`代表外部用户,默认为`sys`
 * @return 如果登录失败,返回`false`;登录成功返回`true`
 */
export function login(userId: string, password: string, userDirectory?: string): boolean;

/**
 * 无密码登录(用户表中还是需要有对应的用户存在)。
 * 
 * 1. 如果当前用户已经是登录状态,那么本函数会先判断当前用户名是否和userId参数一致,如果一致,则什么都不做;如果不一致,则会先注销登录,再用新的用户名登录。
 * 2. 如果想总是先注销登录(不管当前登录用户的用户名是否跟userId一致),再用新用户名登录,那么请先调用security.logout(),再调用本方法
 * 3. 如果想在当前用户已经登录时不用新的用户名登录,那么要这么写: !security.isLogin() && security.loginWithoutPassword(userId);
 * 
 * @param userId 
 * @param userDirectory  用户类型,`sys`代表系统用户,`external`代表外部用户,默认为`sys`
 * @param loginType 所使用的登录方式,不传代表未知
 * 		系统自带的可用值有:
 * 		- "password" : 使用账号+密码或者手机号+密码 登录
 * 		- "pw&sms" : 使用账号+密码登录后,再次使用了手机验证码登录
 * 		- "sms": 使用手机验证码登录
 * 		- "qrcode": 扫码二维码登录
 * 		- "sso": 通过单点登录直接从第三方系统跳转登录
 * 		- "noneuser": 通过 {@link loginWithoutUser} 登录,系统不存储用户
 * @return 如果登录失败,返回`false`;登录成功返回`true`
 */
export function loginWithoutPassword(userId: string, userDirectory?: string, loginType?: string): boolean;

/**
 * 用一个临时的用户登录(用户表中不需要有对应的用户存在,也不验证它的密码)。
 *
 * 使用场景:
 * 1. 适用于用户登录量很大,系统不需要在数据库专门记录用户的信息,通过内存中的信息足够“标示”一个用
 *    户,用户也只是只是临时登录一下使用系统
 * 2. 例如,系统需要和某app做接口,在某app中加一个入口模块,点击后它会通过url中传递一个"token",系统
 *    后台可以根据token获取用户信息并登录,用户量很大,比如1亿
 * 
 * @param user 要登录的用户的信息
 * @param groups 要登录的用户所属的用户组,如果传递了,那么将不读取系统默认的用户组成员表
 * @param userDirectory 用户目录,`sys`代表系统用户,`external`代表外部用户,默认为`sys`
 */
export function loginWithoutUser(user: UserInfo, groups?: string[], userDirectory?: string): boolean;

/**
 * 根据用户ID获取一个用户对象
 * 
 * @param id 用户id,大小写敏感
 * @param userDirectory  用户类型,`sys`代表系统用户,`external`代表外部用户,默认为`sys`
 * @return 用户对象,不存在则返回`null`
 */
export function getUser(id: string, userDirectory?: string): UserInfo;

/**
 * 根据ID批量获取用户信息
 *
 * 使用场景:
 * 1. 完成构建工作流后,工作流配置文件中存储有已经选定的审批人、填报人的信息,为保证再次查看或编辑工
 *    作流时,所展示审批人和填报人信息准确,工作流配置文件中所存储的用户信息仅为`USER_ID`,再次加载
 *    工作流的时候查询获取最新的完整用户信息
 * @param userIds 用户id集合
 * @param userDirectory 用户目录,`sys`代表系统用户,`external`代表外部用户,默认为`sys`
 * @return 都不存在返回空数组 
 */
export function getUsers(userIds: Array<string>, userDirectory?: string): Array<UserInfo>;

/**
 * 列出所有的用户
 * 
 * @param userDirectory 用户目录,`sys`代表系统用户,`external`代表外部用户,默认为`sys`
 * @return
 */
export function getAllUsers(userDirectory?: string): Array<UserInfo>;

/**
 * 添加新用户
 * 
 * - 添加后会刷新该用户被匹配到用户组列表
 * - `user`参数中的属性名使用数据库字段命名方式,比如:数据库字段`USER_ID`,指定为 {USER_ID: "example"},代表添加用户id为`example`的用户
 * 
 * @param user
 * @param userDirectory 用户目录,`sys`代表系统用户,`external`代表外部用户,默认为`sys`
 * @return 添加用户的id
 */
export function addUser(user: UserInfo, userDirectory?: string): string;

/**
 * 删除用户
 * 
 * 同步删除用户组关联、用户状态信息及为用户特殊分配的权限条目
 * @param userId
 * @param userDirectory 用户目录,`sys`代表系统用户,`external`代表外部用户,默认为`sys`
 */
export function deleteUser(userId: string, userDirectory?: string): void;

/**
 * 更新用户信息。
 *
 * 规定:
 * 1. 这个接口将全量更新用户表属性,如果user中不存在或者属性指定为`null`,都将清空属性
 * 2. `user`参数中的属性名使用数据库字段命名方式,比如:数据库字段  USER_ID,指定为 {USER_ID: "example"},代表添加用户id为example的用户
 * 3. 属性更改后会刷新该用户被匹配到的用户组列表
 * @param user
 * @param userDirectory 用户目录,`sys`代表系统用户,`external`代表外部用户,默认为`sys`
 */
export function updateUser(user: UserInfo, userDirectory?: string): void;

/**
 * 获取对应的部门信息
 * 
 * @param id 部门ID
 * @return
 */
export function getDept(id: string): DeptInfo;

/**
 * 获取用户所属的所有用户组
 * 
 * @param userId 用户id
 * @param userDirectory 用户目录,`sys`代表系统用户,`external`代表外部用户,默认为`sys`
 */
export function getGroupsOfUser(userId: string, userDirectory?: string): UserGroupInfo[];

/**
 * 校验登录所使用的 access_token 是否有效
 * 
 * @param access_token 
 */
export function checkAccessToken(access_token: string): {
	/** 授权用户的用户信息 */
	user: UserInfo;
	/**
	 * 是否 access_token 有效
	 */
	valid: boolean;
	/**
	 * access_token 无效的时候,给出的错误码
	 */
	errorCode: string;
	/**
	 * access_token 无效的时候给出的文本提示信息
	 */
	errorMessage: string;
}

/**
 * 向某个手机号码或者邮箱发送一个验证码。
 * 
 * 1. 发送短信或者邮件都需要一个“模版”(大部分短信平台都需要这个,为了杜绝垃圾短信)
 * 2. 参数`templateCode`用于指定模版,验证码会作为一个参数传递给模版,参数名为`code`,验证码由系统自动产生
 * 3. 不能太频繁的对同一个手机发送短信,系统默认 1分钟(具体可以在系统设置中修改)内不能重复发送短信
 * 4. 同一个登录会话(根据JSessionID判断)也不能太频繁发送短信,限制时间和第三条一致
 * 5. 验证手机号码并不要求用户一定登录(比如通过手机短信找回密码)
 * 
 * @param args.phone 手机号
 * @param args.email 邮箱 ,和手机号必须要传一个
 * @param args.useType 表示此验证码的用途,可以为空,验证的时候可以使用,用于附加的判断在验证验证码的时候是否符合实际期望用途
 * @param args.templateCode 用于指定短信模版,验证码会作为一个参数传递给短信模版
 * @param args.verifyCodeParamName 验证码参数在消息模板中对应的参数名称,如果不设置,默认使用 `code`
 */
export function sendVerificationCode(args: { phone?: string; email?: string; useType?: string, templateCode: string, verifyCodeParamName?: string }): {
	/** 是否发送成功 */
	result: boolean;
	/** 发送失败后的错误码 */
	errorCode?: string;
	/** 发送失败后的文本提示 */
	message?: string;
};

/**
 * 验证用户输入的验证码是否正确,以此判断手机是否是当前登录用户的。多用于用户在上一个页面已经输入过了验证码验证过了手机。
 * 
 * 用此方法替代原`checkPhoneVerificationCode`方法。
 * 
 * 1. 验证手机号码并不要求用户一定登录(比如通过手机短信找回密码)
 * 2. 如果先后多次发送过验证码,那么只验证最后一次发送的
 * 3. 如果连续验证失败,将锁定一定时间(系统设置可配置)不允许验证
 * 4. 手机短信/邮件验证有时效性限制,用户必须在指定的时间内验证(通常是10分钟),否则验证码失效
 * 5. 手机短信/邮件验证有时效性限制,用户必须在当前会话中验证,即发送验证码的会话和验证的是一个会话
 * 
 * @param args.phone 手机号
 * @param args.email 邮箱,和手机号必须要传一个
 * @param args.code 验证码
 * @param args.userType 使用途径
 */
export function checkVerificationCode(args: { phone?: string; email?: string; code: string; useType?: string }): {
	/** 校验是否成功 */
	result: boolean;

	/** 错误码 */
	errorCode: string;

	/** 错误信息 */
	message: string;
};

/**
 * 使用用户id生成登录token。
 * 
 * @param userId 
 * @param userDirectory 
 * @returns 如果没有对应用户,则返回`null`
 */
export function createAccessToken(userId: string, userDirectory?: string): string | null;

/**
 * 根据系统设置的用户密码加密算法加密密码
 *
 * 系统加密算法设置索引:项目列表->系统设置->安全->安全设置->密码存储
 *
 * 支持`sha1`、`md5`、`sm3`、`sm4`以及自定义脚本加密,默认采用`sha1`进行加密。
 *
 * 脚本加密设置参见链接中的`encryptUserPassword`方法:
 *     https://docs.succbi.com/dev/hooks/security-action/
 *
 * @param password 密码
 * @returns 加密后的内容
 */
export function encryptUserPassword(password: string): string;

/**
 * 对比输入的明文密码是否与数据库记录的密码一致
 * 
 * @param plainPassword 明文密码
 * @param encryptedPassword 加密后的用户信息
 * @param salt 盐字段
 * @returns
 */
export function compareUserPassword(plainPassword: string, encryptedPassword: string, salt?: string): string;

/**
 * 在当前线程中模拟一个web线程的登录
 *
 * 使用场景:
 * 1. 需要定时执行一个任务,任务需要导出一个报表为`excel`文件,然后将文件作为消息发送出去:考虑到,
 *     消息可能发送给不同用户,不同用户看到的报表的数据范围也是不一样的,通过修改当前执行任务的登录
 *     上下文,可以切换用户,然后根据用户被设置的权限来控制导出数据的范围
 *
 * 说明:
 * 1. 所有需要记录用户操作、判断用户权限的操作都需要有WEB现成上下文, 比如执行一个定时任务、记录操作
 *    日志等
 * 2. 如果本身就在WEB线程中调用本方法,也会替换原先的上下文,可以通过调用
 *    {@link cleanLoginSessionBean}来还原
 *
 * 常规使用写法为:
 * ```code
 *   import security from 'svr-api/security'
 *
 *   let mockUserId = 'mockUserId';//待用来模拟登录的用户id
 *   let currentUserId = security.getCurrentUser().userInfo.userId;// 获取到当前线程中登录的用户id
 *   security.mockLoginSessionBean(mockUserId);
 *   // xxx 处理业务
 *   security.mockLoginSessionBean(currentUserId);//将登录还原到处理任务之前
 *   // xxx 处理其他业务
 * ```
 *
 * @param userId 用来登录的用户ID,只能是系统用户不能是外部用户,如果用户不存在或者被禁用,这个方法
 *     会抛出异常
 */
export function mockLoginSessionBean(userId: string): void;

/**
 * 清理当前线程中的登录信息。
 *
 * 1. 如果本身就在web线程中,调用{@link mockLoginSessionBean} 后,再调用本方法,会还原之前的WEB线程
 *    上下文;
 * 2. 如果本身不在web线程中,直接调用本方法,将什么都不做。
 */
export function cleanLoginSessionBean(): void;

/**
 * 发起退款
 * 
 * 可以发起多次,总金额不得大于付款金额。
 * 
 * @param orderId 订单编号
 * @param reason 退款原因
 * @param amount 退款金额
 * @return 
 */
export function refund(orderId: string, reason: string, amount: number): {
	/** 退款是否成功 */
	result: boolean;

	/** 错误码 */
	errorCode: string;

	/** 错误信息 */
	message: string;
};

# 对象

/**
 * 此文件定义一些安全相关的数据类型。
 * 
 */


/**
 * 消息扩展点实现者在发送消息时所接受到的参数
 */
declare interface SendMessageArgs {
	//消息的标题,可选,比如短信消息就不需要标题
	subject?: string,
	//消息体
	content: string,
	//目标用户,根据参数中的用户信息,实现者可以取得用户的邮件、电话等信息
	users: Array<UserInfo>,
}

/**
 * 当前用户对象
 */
declare interface CurrentUser {

    /**
     * 是否为管理员
     */
	readonly isAdmin: boolean;

    /**
     * 用户信息
     */
	userInfo: UserInfo;

	/**
	 * 设置用户信息
	 *
	 * 说明:此方法不允许修改userId属性
	 * @param propertyName 用户信息属性
	 * @param value 值
	 */
	setUserProperty(propertyName: string, value: any): void;

	/**
	 * 获取当前用户所在用户组信息
	 */
	getGroups(): Array<UserGroupInfo>;

	/**
	 * 修改用户对一个资源或者一批资源的权限分配
	 *
	 * 注意:
	 *
	 * 1. 调用方法后,即时生效;特殊注意如果有第二次调用,还是基于`第一次调用前`的用户权限作出修改,请在调用时尽
	 *    量一次性的将要添加的权限通过一次函数调用传入
	 * 2. 若用户对指定的资源已经有权限,调用方法将会覆盖原有的权限;用户对资源没有权限,则认为是添加
	 * 3. 如果修改的是一个目录的权限,并且用户对这个目录的下级资源也单独被分配了权限,根据下级资源的
	 *    权限继承所在目录的权限的规则,修改后可能会影响到用户对下级资源的权限
	 *
	 * 常规使用场景:
	 *
	 * 1. 用户的权限可能存储在第三方系统,定制
	 *    (https://docs.succbi.com/v5/dev/script/security-action/)中的`onDidSignin`钩子方法,从第
	 *    三方系统获取权限数据设置给用户
	 * 2. 在特殊场景下为用户对某个资源临时分配权限
	 *
	 * @param permissions 权限数据
	 */
	modifyPermissions(permissions: succ.security.PermissionItemInfo | succ.security.PermissionItemInfo[]): void;

	/**
	 * 重置用户的权限为权限表中所存储的权限
	 *
	 * 注意:如果已经在一些脚本钩子函数(比如 https://docs.succbi.com/v5/dev/script/security-action/
	 * 中的`onDidSignin`方法)修改了用户的权限,调用本函数后不会重新调用这个些钩子函数,脚本开发者需
	 * 要考虑是否需要重新加上
	 *
	 * 1. 系统的权限管理界面不支持在用户权限修改后,即时作用到已经登录的用户上,可以调用在脚本中调用
	 *    当前方法达到目的
	 * 2. 通过{@link modifyPermissions}方法可能只是为了临时修改个信息,并不想一直作用,可以调用当前
	 *    方法回退权限修改
	 */
	resetPermission(): void;

	/**
	 * 用于获取用户存储在后端的一个数据。
	 * 
	 * 有时候二次开发需要一个能按用户存储数据的接口,数据存储在后端,下次登录还可以使用到。
	 * 
	 * @param key 对应的key,长度不能超过256,不能为空,大小写敏感,不同的用户key不会冲突。
	 */
	getStorageValue(key: string): Promise<any>;

	/**
	 * 用途见{@link getStorageValue()}函数的注释。
	 * 
	 * @param key 
	 * @param value 要存储的数据,任意类型都可以,长度太大(超过1k)时后端会存储到clob中去。
	 */
	saveStorageValue(key: string, value: any): Promise<void>;

	/**
	 * 注销登录
	 */
	logout(): void;

	/**
	 * 检查当前登录用户是否有资源的操作权限
	 * @param path 资源路径
	 * @param operation 权限操作,如:查看("view-basic") 编辑("mgr-m") 导出("mgr-m-export") 保存("mgr-m-save")
	 * @param throwIfFail 没有对应权限的时候是否抛出ForbiddenException
	 * @returns 
	 */
	checkAllowed(path: string, operation: string, throwIfFail?: boolean): boolean;

	/**
	 * 判断是否拥有一项资源的浏览权限{@link PermissionOperation.browse}
	 *
	 * 对资源有浏览权限代表用户可以知道资源存在,也可以知道一些资源本身的元数据信息,如文件名、描
	 * 述、文件信息等,但是不能浏览其下级资源、不能查看资源内容、更不能修改;浏览权限与查看权限
	 * {@link PermissionOperation.View_basic}不同,查看权限表示可以查看文件的内容,当然也可以获取文
	 * 件的元数据信息,它是包含浏览权限的;另外只要对资源有任意权限,就认为对资源有浏览权限。
	 *
	 * @param path 资源路径
	 * @param throwIfFail 没有浏览权限的时候是否抛出ForbiddenException
	 * @return
	 */
	checkBrowsable(path: string, throwIfFail?: boolean): boolean;

	/**
	 * 获取当前用户对一个资源拥有的权限数据范围
	 *
	 * 1. 根据权限分配脚本自己实现查询引擎
	 * 2. 用于测试:浏览器中可以获取到资源的权限分配状况,但是没有数据范围数据,这个数据用来确认后台
	 *    存储的数据范围是否符合预期
	 *
	 * 返回null可能是没有权限,也可能是没有数据范围设置可以操作全部数据,判断是否有权限请先调用
	 * {@link checkAllowed}
	 *
	 * @param path 资源路径
	 * @param operation 权限操作,不指定默认为查看权限{@link PermissionOperation.View_basic}
	 * @returns
	 */
	getDataRange(path: string, operation?: succ.security.PermissionOperation): succ.security.PermissionDataRange | null;

	/**
	 * 检查是否对一个资源有对应操作权限
	 *
	 * 用于需要同时判断对一个资源是否有多项权限设置的业务操作进行权限判断调用,比如`查看报表操作`需要
	 * 进行的判断逻辑有:
	 *
	 * 1. 校验对应`报表文件`是否存在
	 * 2. 判断用户对报表是否有查看权限
	 * 3. 展示报表数据时,需要根据数据范围过滤出用户可以查看到的数据
	 *
	 * 这三个校验步骤的记过都可以通过本函数的返回值{@link CheckPermissionResult}获取到,并且本方
	 * 法只搜索一次权限列表
	 *
	 * @param path 资源路径
	 * @param operation 权限操作
	 * @returns 不会返回null
	 */
	checkPermission(path: string, operation: string): CheckPermissionResult
}

/**
 * 权限判断的结果对象
 */
interface CheckPermissionResult {

	/**
	 * 获取权限检查结果
	 *
	 * @return true代表有权限,false代表没有权限
	 */
	getResult(): boolean;

	/**
	 * 如果{@link #getResult}返回false,则调用此方法会抛出异常
	 */
	checkResult(): void;

	/**
	 * 获取权限所对应的的数据范围设置
	 *
	 * 1. 当{@link #getResult()} 返回true时,返回用来获取限制所判断权限的数据范围,但是也可能因为没
	 *    有设置数据范围,返回null
	 * 2. 当{@link #getResult()} 返回false时,抛出异常
	 *
	 * @returns 返回null代表没有范围限制
	 */
	getDataRange(): succ.security.PermissionDataRange | null;

	/**
	 * 获取进行权限判断资源所对应元数据文件信息。
	 *
	 * 调用此函数与直接调用{@link metadata.getFile}获取文件不同,此函数会在文件不存在时根据相关权限
	 * 设置返回一个虚构的文件信息。
	 *
	 * 如在给用户动态分配权限时,在`/ana/users`目录下,动态的给每个用户分配一个目录名为自己的用户id
	 * 的目录,可以自己新建和管理自己的分析文件,那么系统初始化时`/ana/users`目录下并未提前创建好所
	 * 有的用户的目录,新注册用户时也不会自动创建,系统会在用户查看自己的资源目录时总是显示自己的这
	 * 个目录(分配权限时需设置{@link succ.security.PermissionItemOptions.alwaysShowFolder}),在界
	 * 面上选择这个目录新建文件等操作时,用户也是无法感知这个目录是不存在的,也就是说这个目录在系统
	 * 提供出去的api层面上要被视为存在。
	 *
	 * 只有当元数据不存在,并且用户权限对这个不存在的资源设置了
	 * {@link succ.security.PermissionItemOptions.alwaysShowFolder},才会返回一个虚拟的文件信息
	 *
	 * 在涉及到权限判断并且还需要确认操作的资源是否存在时,调用本函数来获取元数据文件,在要操作数据
	 * 库查询或者修改元数据时,还是需要使用{@link metadata.getFile}来获取元数据
	 *
	 * 注意:
	 *
	 * 1. 如果确认了资源是系统资源(比如系统设置页面`/syssettings`),不要调用此函数,必然会抛出异常
	 *    或者返回null
	 * 2. 如果检查权限({@link #getResult()}返回false)失败,必然抛出异常
	 *
	 * @param throwIfNotExist 是否在资源对应元数据为了将不存在时抛出异常
	 * @return 可能返回null
	 */
	getMetaFile(throwIfNotExist: boolean): MetaFileInfo;

	/**
	 * 是否在文件夹下面新建文件时自动创建此文件夹(如果它不存在的话)。
	 *
	 * 如在给用户动态分配权限时,在`/ana/users`目录下,动态的给每个用户分配一个目录名为自己的用户id
	 * 的目录,可以自己新建和管理自己的分析文件,那么系统初始化时`/ana/users`目录下并未提前创建好所
	 * 有的用户的目录,新注册用户时也不会自动创建,系统会在用户查看自己的资源目录时总是显示自己的这
	 * 个目录(分配权限时需设置{@link succ.security.PermissionItemOptions.alwaysShowFolder}),且在
	 * 用户首次在此目录下新建文件时自动创建它。
	 *
	 * 注意,自动创建目录的前提是:
	 *
	 * 1. 目录不存在
	 * 2. 用户对目录有显式分配的权限,且对下面的文件有新建权限。
	 * 3. 且是在目录的直接下级新建文件,不能越级。
	 * 4. 如果检查权限{@link #getResult()}失败,抛出异常
	 *
	 * @return 返回true时,如果在此目录下新建(直属的)文件时目录不存在,那么新建它
	 */
	needAutoCreateFolder(): boolean;

	/**
	 * 检查操作是否可对文件类型起作用
	 *
	 * 用于新建文件时确认是否可以新建对应的类型的文件
	 *
	 * 注意:如果检查权限失败({@link #getResult()}返回false),会直接抛出异常,而不是根据参数
	 * `throwIfFail`来决定是否抛出异常
	 *
	 * 参数`type`:为文件类型,兼容{@link MetaFileInfo.type},传入`folder`代表是个目录,`app`文件比
	 * 较特殊虽然是目录,但也是有类型的,这里应该传递`app` {@link MetaFileType.APP}
	 *
	 * @param type 文件类型
	 * @param throwIfFail 是否在文件类型不允许操作后抛出异常
	 * @returns true代表允许操作指定类型
	 */
	checkFileType(type: string, throwIfFail: boolean): void;
}
是否有帮助?
0条评论
评论