diff --git a/api/dms/service/v1/configuration.go b/api/dms/service/v1/configuration.go index fd5aa09cd..1ceec6adf 100644 --- a/api/dms/service/v1/configuration.go +++ b/api/dms/service/v1/configuration.go @@ -11,8 +11,9 @@ type GetLoginTipsReply struct { } type LoginTipsResData struct { - LoginButtonText string `json:"login_button_text"` - DisableUserPwdLogin bool `json:"disable_user_pwd_login"` + LoginButtonText string `json:"login_button_text"` + DisableUserPwdLogin bool `json:"disable_user_pwd_login"` + DisableMultipleLogin bool `json:"disable_multiple_login"` } // swagger:model @@ -21,8 +22,9 @@ type UpdateLoginConfigurationReq struct { } type LoginConfiguration struct { - LoginButtonText *string `json:"login_button_text"` - DisableUserPwdLogin *bool `json:"disable_user_pwd_login"` + LoginButtonText *string `json:"login_button_text"` + DisableUserPwdLogin *bool `json:"disable_user_pwd_login"` + DisableMultipleLogin *bool `json:"disable_multiple_login"` } type GetOauth2ConfigurationResData struct { diff --git a/api/swagger.json b/api/swagger.json index d2bd581e3..e55c74f27 100644 --- a/api/swagger.json +++ b/api/swagger.json @@ -1615,6 +1615,55 @@ } } }, + "/v1/dms/masking/rules": { + "get": { + "tags": [ + "Masking" + ], + "summary": "查询脱敏规则列表(兼容旧入口,仅返回内置规则,可携带筛选参数)。", + "operationId": "ListMaskingRulesWithoutProject", + "parameters": [ + { + "type": "string", + "description": "规则来源筛选,builtin 或 custom,为空时返回全部", + "name": "source", + "in": "query" + }, + { + "type": "string", + "description": "模糊搜索关键字,匹配规则名称、描述或敏感数据类型名称", + "name": "keywords", + "in": "query" + }, + { + "type": "integer", + "description": "分页大小,默认 20", + "name": "page_size", + "in": "query" + }, + { + "type": "integer", + "description": "页码(从1开始),默认 1", + "name": "page_index", + "in": "query" + } + ], + "responses": { + "200": { + "description": "查询脱敏规则列表成功", + "schema": { + "$ref": "#/definitions/ListMaskingRulesReply" + } + }, + "default": { + "description": "通用错误响应", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, "/v1/dms/notifications": { "post": { "tags": [ @@ -3618,6 +3667,51 @@ } } }, + "/v1/dms/projects/{project_uid}/data_export_workflows/{data_export_workflow_uid}/original-export/download": { + "get": { + "tags": [ + "DataExportWorkflows" + ], + "summary": "Download unmasked SQL query results as a zip file. Each request runs export in memory; files are not persisted.", + "operationId": "DownloadOriginalDataExportWorkflow", + "parameters": [ + { + "type": "string", + "x-go-name": "ProjectUid", + "description": "project id", + "name": "project_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "x-go-name": "DataExportWorkflowUid", + "name": "data_export_workflow_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "x-go-name": "UnmaskingWorkflowUid", + "description": "已批准的查看原文工单 UID", + "name": "unmasking_workflow_uid", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/DownloadOriginalDataExportWorkflowReply" + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, "/v1/dms/projects/{project_uid}/data_export_workflows/{data_export_workflow_uid}/reject": { "post": { "tags": [ @@ -4186,6 +4280,59 @@ } } }, + "/v1/dms/projects/{project_uid}/db_services/{db_service_uid}/schemas/{schema_name}/tables/{table_name}/columns": { + "get": { + "tags": [ + "DBStructure" + ], + "summary": "List table columns (internal API for lineage analysis).", + "operationId": "ListTableColumns", + "parameters": [ + { + "type": "string", + "x-go-name": "ProjectUid", + "name": "project_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "x-go-name": "DBServiceUid", + "name": "db_service_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "x-go-name": "SchemaName", + "name": "schema_name", + "in": "path", + "required": true + }, + { + "type": "string", + "x-go-name": "TableName", + "name": "table_name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "ListTableColumnsReply", + "schema": { + "$ref": "#/definitions/ListTableColumnsReply" + } + }, + "default": { + "description": "GenericResp", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, "/v1/dms/projects/{project_uid}/environment_tags": { "get": { "tags": [ @@ -5732,6 +5879,45 @@ } } }, + "/v1/dms/projects/{project_uid}/masking/unmasking-workflows/{workflow_id}/activate": { + "post": { + "tags": [ + "Masking" + ], + "summary": "Activate plaintext view window for applicant.", + "operationId": "ActivateUnmaskingWorkflowView", + "parameters": [ + { + "type": "string", + "x-go-name": "WorkflowID", + "name": "project_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "x-go-name": "WorkflowID", + "name": "workflow_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ActivateUnmaskingWorkflowViewReply" + } + }, + "default": { + "description": "", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, "/v1/dms/projects/{project_uid}/masking/unmasking-workflows/{workflow_id}/approve": { "post": { "tags": [ @@ -5820,6 +6006,45 @@ } } }, + "/v1/dms/projects/{project_uid}/masking/unmasking-workflows/{workflow_id}/plaintext": { + "get": { + "tags": [ + "Masking" + ], + "summary": "Get plaintext query snapshot during active view window.", + "operationId": "GetUnmaskingWorkflowPlaintext", + "parameters": [ + { + "type": "string", + "x-go-name": "WorkflowID", + "name": "project_uid", + "in": "path", + "required": true + }, + { + "type": "string", + "x-go-name": "WorkflowID", + "name": "workflow_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/GetUnmaskingWorkflowPlaintextReply" + } + }, + "default": { + "description": "", + "schema": { + "$ref": "#/definitions/GenericResp" + } + } + } + } + }, "/v1/dms/projects/{project_uid}/masking/unmasking-workflows/{workflow_id}/reject": { "post": { "tags": [ @@ -8387,6 +8612,47 @@ }, "x-go-package": "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" }, + "ActivateUnmaskingWorkflowViewReply": { + "type": "object", + "properties": { + "code": { + "description": "code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "data": { + "$ref": "#/definitions/ActivateUnmaskingWorkflowViewReplyData" + }, + "message": { + "description": "message", + "type": "string", + "x-go-name": "Message" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, + "ActivateUnmaskingWorkflowViewReplyData": { + "type": "object", + "properties": { + "view_state": { + "type": "string", + "enum": [ + "not_activated", + "active", + "view_expired", + "activation_expired" + ], + "x-go-enum-desc": "not_activated UnmaskingWorkflowViewStateNotActivated\nactive UnmaskingWorkflowViewStateActive\nview_expired UnmaskingWorkflowViewStateViewExpired\nactivation_expired UnmaskingWorkflowViewStateActivationExpired", + "x-go-name": "ViewState" + }, + "view_valid_until": { + "type": "string", + "x-go-name": "ViewValidUntil" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "AddDBServiceReply": { "type": "object", "properties": { @@ -10793,6 +11059,70 @@ }, "x-go-package": "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" }, + "DataExportRelatedUnmaskingWorkflow": { + "type": "object", + "properties": { + "apply_reason": { + "description": "申请理由", + "type": "string", + "x-go-name": "ApplyReason" + }, + "approval_status": { + "description": "审批状态\npending UnmaskingWorkflowApprovalStatusPending 待审批\napproved UnmaskingWorkflowApprovalStatusApproved 已批准\nrejected UnmaskingWorkflowApprovalStatusRejected 已驳回\ncancelled UnmaskingWorkflowApprovalStatusCancelled 已取消", + "type": "string", + "enum": [ + "pending", + "approved", + "rejected", + "cancelled" + ], + "x-go-enum-desc": "pending UnmaskingWorkflowApprovalStatusPending 待审批\napproved UnmaskingWorkflowApprovalStatusApproved 已批准\nrejected UnmaskingWorkflowApprovalStatusRejected 已驳回\ncancelled UnmaskingWorkflowApprovalStatusCancelled 已取消", + "x-go-name": "ApprovalStatus" + }, + "created_at": { + "description": "创建时间 (RFC3339)", + "type": "string", + "x-go-name": "CreatedAt" + }, + "creator": { + "$ref": "#/definitions/UidWithName" + }, + "expire_time": { + "description": "过期时间 (RFC3339),未设置时省略", + "type": "string", + "x-go-name": "ExpireTime" + }, + "operation_logs": { + "description": "操作记录", + "type": "array", + "items": { + "$ref": "#/definitions/UnmaskingOperationLogItem" + }, + "x-go-name": "OperationLogs" + }, + "reject_reason": { + "description": "驳回理由(整单驳回时)", + "type": "string", + "x-go-name": "RejectReason" + }, + "unmasking_workflow_uid": { + "description": "UnmaskingWorkflowUid 查看原文工单 UID,用于跳转详情等", + "type": "string", + "x-go-name": "UnmaskingWorkflowUid" + }, + "usage_status": { + "description": "使用状态\nunviewed UnmaskingWorkflowUsageStatusUnviewed 未查看\nviewed UnmaskingWorkflowUsageStatusViewed 已查看", + "type": "string", + "enum": [ + "unviewed", + "viewed" + ], + "x-go-enum-desc": "unviewed UnmaskingWorkflowUsageStatusUnviewed 未查看\nviewed UnmaskingWorkflowUsageStatusViewed 已查看", + "x-go-name": "UsageStatus" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "DataExportTask": { "type": "object", "required": [ @@ -11335,6 +11665,9 @@ "type": "string", "x-go-name": "Desc" }, + "unmasking_workflow": { + "$ref": "#/definitions/DataExportRelatedUnmaskingWorkflow" + }, "workflow_name": { "type": "string", "x-go-name": "Name" @@ -12084,6 +12417,54 @@ }, "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" }, + "GetUnmaskingWorkflowPlaintext": { + "type": "object", + "properties": { + "unmasking_sqls": { + "type": "array", + "items": { + "$ref": "#/definitions/UnmaskingPlaintextSQLItem" + }, + "x-go-name": "UnmaskingSQLs" + }, + "view_state": { + "type": "string", + "enum": [ + "not_activated", + "active", + "view_expired", + "activation_expired" + ], + "x-go-enum-desc": "not_activated UnmaskingWorkflowViewStateNotActivated\nactive UnmaskingWorkflowViewStateActive\nview_expired UnmaskingWorkflowViewStateViewExpired\nactivation_expired UnmaskingWorkflowViewStateActivationExpired", + "x-go-name": "ViewState" + }, + "view_valid_until": { + "type": "string", + "x-go-name": "ViewValidUntil" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, + "GetUnmaskingWorkflowPlaintextReply": { + "type": "object", + "properties": { + "code": { + "description": "code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "data": { + "$ref": "#/definitions/GetUnmaskingWorkflowPlaintext" + }, + "message": { + "description": "message", + "type": "string", + "x-go-name": "Message" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "GetUnmaskingWorkflowReply": { "type": "object", "properties": { @@ -15416,6 +15797,30 @@ }, "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" }, + "ListTableColumnsReply": { + "type": "object", + "properties": { + "code": { + "description": "code", + "type": "integer", + "format": "int64", + "x-go-name": "Code" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/TableColumn" + }, + "x-go-name": "Data" + }, + "message": { + "description": "message", + "type": "string", + "x-go-name": "Message" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "ListUnmaskingWorkflowsReply": { "type": "object", "properties": { @@ -15659,6 +16064,10 @@ "LoginConfiguration": { "type": "object", "properties": { + "disable_multiple_login": { + "type": "boolean", + "x-go-name": "DisableMultipleLogin" + }, "disable_user_pwd_login": { "type": "boolean", "x-go-name": "DisableUserPwdLogin" @@ -15673,6 +16082,10 @@ "LoginTipsResData": { "type": "object", "properties": { + "disable_multiple_login": { + "type": "boolean", + "x-go-name": "DisableMultipleLogin" + }, "disable_user_pwd_login": { "type": "boolean", "x-go-name": "DisableUserPwdLogin" @@ -17815,6 +18228,28 @@ }, "x-go-package": "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" }, + "TableColumn": { + "type": "object", + "properties": { + "comment": { + "type": "string", + "x-go-name": "Comment" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "nullable": { + "type": "boolean", + "x-go-name": "Nullable" + }, + "type": { + "type": "string", + "x-go-name": "Type" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "TableColumnMaskingDetail": { "type": "object", "properties": { @@ -18314,13 +18749,13 @@ "x-go-name": "Uid" } }, - "x-go-package": "github.com/actiontech/dms/pkg/dms-common/api/dms/v1" + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" }, "UnmaskingOperationLogItem": { "type": "object", "properties": { "action": { - "description": "操作动作\nsubmit UnmaskingActionSubmit 提交申请\napprove UnmaskingActionApprove 批准申请\nreject UnmaskingActionReject 驳回申请\nview_unmasking_workflow_detail UnmaskingActionViewOriginalDataWorkflowDetail 查看工单详情\nview_full_original_data UnmaskingActionViewOriginalData 查看原文\ndownload_full_original_data UnmaskingActionDownloadOriginalData 下载原文\ncancel UnmaskingActionCancel 取消申请", + "description": "操作动作\nsubmit UnmaskingActionSubmit 提交申请\napprove UnmaskingActionApprove 批准申请\nreject UnmaskingActionReject 驳回申请\nview_unmasking_workflow_detail UnmaskingActionViewOriginalDataWorkflowDetail 查看工单详情\nview_full_original_data UnmaskingActionViewOriginalData 查看原文\ndownload_full_original_data UnmaskingActionDownloadOriginalData 下载原文\ncancel UnmaskingActionCancel 取消申请\nactivate_view UnmaskingActionActivateView 激活查看原文(工单详情)", "type": "string", "enum": [ "submit", @@ -18329,9 +18764,10 @@ "view_unmasking_workflow_detail", "view_full_original_data", "download_full_original_data", - "cancel" + "cancel", + "activate_view" ], - "x-go-enum-desc": "submit UnmaskingActionSubmit 提交申请\napprove UnmaskingActionApprove 批准申请\nreject UnmaskingActionReject 驳回申请\nview_unmasking_workflow_detail UnmaskingActionViewOriginalDataWorkflowDetail 查看工单详情\nview_full_original_data UnmaskingActionViewOriginalData 查看原文\ndownload_full_original_data UnmaskingActionDownloadOriginalData 下载原文\ncancel UnmaskingActionCancel 取消申请", + "x-go-enum-desc": "submit UnmaskingActionSubmit 提交申请\napprove UnmaskingActionApprove 批准申请\nreject UnmaskingActionReject 驳回申请\nview_unmasking_workflow_detail UnmaskingActionViewOriginalDataWorkflowDetail 查看工单详情\nview_full_original_data UnmaskingActionViewOriginalData 查看原文\ndownload_full_original_data UnmaskingActionDownloadOriginalData 下载原文\ncancel UnmaskingActionCancel 取消申请\nactivate_view UnmaskingActionActivateView 激活查看原文(工单详情)", "x-go-name": "Action" }, "action_time": { @@ -18357,6 +18793,34 @@ }, "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" }, + "UnmaskingPlaintextSQLItem": { + "type": "object", + "properties": { + "masked_columns": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "MaskedColumns" + }, + "original_data": { + "$ref": "#/definitions/SQLQueryResult" + }, + "sql_index_id": { + "type": "string", + "x-go-name": "SQLIndexID" + }, + "truncated": { + "type": "boolean", + "x-go-name": "Truncated" + }, + "uid": { + "type": "string", + "x-go-name": "UID" + } + }, + "x-go-package": "github.com/actiontech/dms/api/dms/service/v1" + }, "UnmaskingSQLDetail": { "type": "object", "properties": { @@ -18398,6 +18862,16 @@ "UnmaskingWorkflowDetail": { "type": "object", "properties": { + "activated_at": { + "description": "激活查看时刻 (RFC3339)", + "type": "string", + "x-go-name": "ActivatedAt" + }, + "activation_deadline": { + "description": "须在此时刻前激活查看 (RFC3339)", + "type": "string", + "x-go-name": "ActivationDeadline" + }, "applicant_name": { "description": "申请人用户名", "type": "string", @@ -18420,6 +18894,11 @@ "x-go-enum-desc": "pending UnmaskingWorkflowApprovalStatusPending 待审批\napproved UnmaskingWorkflowApprovalStatusApproved 已批准\nrejected UnmaskingWorkflowApprovalStatusRejected 已驳回\ncancelled UnmaskingWorkflowApprovalStatusCancelled 已取消", "x-go-name": "ApprovalStatus" }, + "can_activate": { + "description": "是否可点击激活查看", + "type": "boolean", + "x-go-name": "CanActivate" + }, "created_at": { "description": "申请时间 (RFC3339)", "type": "string", @@ -18444,7 +18923,7 @@ "x-go-name": "DatasourceUid" }, "expire_time": { - "description": "过期时间 (RFC3339)", + "description": "过期时间 (RFC3339),兼容字段:批准前为激活截止,激活后为查看截止", "type": "string", "x-go-name": "ExpireTime" }, @@ -18494,6 +18973,23 @@ "x-go-enum-desc": "unviewed UnmaskingWorkflowUsageStatusUnviewed 未查看\nviewed UnmaskingWorkflowUsageStatusViewed 已查看", "x-go-name": "UsageStatus" }, + "view_state": { + "description": "申请人明文查看状态\nnot_activated UnmaskingWorkflowViewStateNotActivated\nactive UnmaskingWorkflowViewStateActive\nview_expired UnmaskingWorkflowViewStateViewExpired\nactivation_expired UnmaskingWorkflowViewStateActivationExpired", + "type": "string", + "enum": [ + "not_activated", + "active", + "view_expired", + "activation_expired" + ], + "x-go-enum-desc": "not_activated UnmaskingWorkflowViewStateNotActivated\nactive UnmaskingWorkflowViewStateActive\nview_expired UnmaskingWorkflowViewStateViewExpired\nactivation_expired UnmaskingWorkflowViewStateActivationExpired", + "x-go-name": "ViewState" + }, + "view_valid_until": { + "description": "明文查看截止 (RFC3339)", + "type": "string", + "x-go-name": "ViewValidUntil" + }, "workflow_id": { "description": "申请编号", "type": "string", @@ -18505,6 +19001,16 @@ "UnmaskingWorkflowListItem": { "type": "object", "properties": { + "activated_at": { + "description": "激活查看时刻 (RFC3339)", + "type": "string", + "x-go-name": "ActivatedAt" + }, + "activation_deadline": { + "description": "须在此时刻前激活查看 (RFC3339)", + "type": "string", + "x-go-name": "ActivationDeadline" + }, "applicant_name": { "description": "申请人用户名", "type": "string", @@ -18527,6 +19033,11 @@ "x-go-enum-desc": "pending UnmaskingWorkflowApprovalStatusPending 待审批\napproved UnmaskingWorkflowApprovalStatusApproved 已批准\nrejected UnmaskingWorkflowApprovalStatusRejected 已驳回\ncancelled UnmaskingWorkflowApprovalStatusCancelled 已取消", "x-go-name": "ApprovalStatus" }, + "can_activate": { + "description": "是否可点击激活查看", + "type": "boolean", + "x-go-name": "CanActivate" + }, "created_at": { "description": "申请时间 (RFC3339)", "type": "string", @@ -18551,7 +19062,7 @@ "x-go-name": "DatasourceUid" }, "expire_time": { - "description": "过期时间 (RFC3339)", + "description": "过期时间 (RFC3339),兼容字段:批准前为激活截止,激活后为查看截止", "type": "string", "x-go-name": "ExpireTime" }, @@ -18580,6 +19091,23 @@ "x-go-enum-desc": "unviewed UnmaskingWorkflowUsageStatusUnviewed 未查看\nviewed UnmaskingWorkflowUsageStatusViewed 已查看", "x-go-name": "UsageStatus" }, + "view_state": { + "description": "申请人明文查看状态\nnot_activated UnmaskingWorkflowViewStateNotActivated\nactive UnmaskingWorkflowViewStateActive\nview_expired UnmaskingWorkflowViewStateViewExpired\nactivation_expired UnmaskingWorkflowViewStateActivationExpired", + "type": "string", + "enum": [ + "not_activated", + "active", + "view_expired", + "activation_expired" + ], + "x-go-enum-desc": "not_activated UnmaskingWorkflowViewStateNotActivated\nactive UnmaskingWorkflowViewStateActive\nview_expired UnmaskingWorkflowViewStateViewExpired\nactivation_expired UnmaskingWorkflowViewStateActivationExpired", + "x-go-name": "ViewState" + }, + "view_valid_until": { + "description": "明文查看截止 (RFC3339)", + "type": "string", + "x-go-name": "ViewValidUntil" + }, "workflow_id": { "description": "申请编号", "type": "string", @@ -20205,6 +20733,12 @@ "type": "file" } }, + "DownloadOriginalDataExportWorkflowReply": { + "description": "", + "schema": { + "type": "file" + } + }, "DownloadResourceOverviewListRes": { "description": "", "schema": { diff --git a/api/swagger.yaml b/api/swagger.yaml index f5a53b320..b2c31571a 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -15,6 +15,41 @@ definitions: x-go-name: ExpiredTime type: object x-go-package: github.com/actiontech/dms/pkg/dms-common/api/dms/v1 + ActivateUnmaskingWorkflowViewReply: + properties: + code: + description: code + format: int64 + type: integer + x-go-name: Code + data: + $ref: '#/definitions/ActivateUnmaskingWorkflowViewReplyData' + message: + description: message + type: string + x-go-name: Message + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 + ActivateUnmaskingWorkflowViewReplyData: + properties: + view_state: + enum: + - not_activated + - active + - view_expired + - activation_expired + type: string + x-go-enum-desc: |- + not_activated UnmaskingWorkflowViewStateNotActivated + active UnmaskingWorkflowViewStateActive + view_expired UnmaskingWorkflowViewStateViewExpired + activation_expired UnmaskingWorkflowViewStateActivationExpired + x-go-name: ViewState + view_valid_until: + type: string + x-go-name: ViewValidUntil + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 AddDBServiceReply: properties: code: @@ -1860,6 +1895,70 @@ definitions: - proxy_url_prefixs type: object x-go-package: github.com/actiontech/dms/pkg/dms-common/api/dms/v1 + DataExportRelatedUnmaskingWorkflow: + properties: + apply_reason: + description: 申请理由 + type: string + x-go-name: ApplyReason + approval_status: + description: |- + 审批状态 + pending UnmaskingWorkflowApprovalStatusPending 待审批 + approved UnmaskingWorkflowApprovalStatusApproved 已批准 + rejected UnmaskingWorkflowApprovalStatusRejected 已驳回 + cancelled UnmaskingWorkflowApprovalStatusCancelled 已取消 + enum: + - pending + - approved + - rejected + - cancelled + type: string + x-go-enum-desc: |- + pending UnmaskingWorkflowApprovalStatusPending 待审批 + approved UnmaskingWorkflowApprovalStatusApproved 已批准 + rejected UnmaskingWorkflowApprovalStatusRejected 已驳回 + cancelled UnmaskingWorkflowApprovalStatusCancelled 已取消 + x-go-name: ApprovalStatus + created_at: + description: 创建时间 (RFC3339) + type: string + x-go-name: CreatedAt + creator: + $ref: '#/definitions/UidWithName' + expire_time: + description: 过期时间 (RFC3339),未设置时省略 + type: string + x-go-name: ExpireTime + operation_logs: + description: 操作记录 + items: + $ref: '#/definitions/UnmaskingOperationLogItem' + type: array + x-go-name: OperationLogs + reject_reason: + description: 驳回理由(整单驳回时) + type: string + x-go-name: RejectReason + unmasking_workflow_uid: + description: UnmaskingWorkflowUid 查看原文工单 UID,用于跳转详情等 + type: string + x-go-name: UnmaskingWorkflowUid + usage_status: + description: |- + 使用状态 + unviewed UnmaskingWorkflowUsageStatusUnviewed 未查看 + viewed UnmaskingWorkflowUsageStatusViewed 已查看 + enum: + - unviewed + - viewed + type: string + x-go-enum-desc: |- + unviewed UnmaskingWorkflowUsageStatusUnviewed 未查看 + viewed UnmaskingWorkflowUsageStatusViewed 已查看 + x-go-name: UsageStatus + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 DataExportTask: properties: database_name: @@ -2279,6 +2378,8 @@ definitions: desc: type: string x-go-name: Desc + unmasking_workflow: + $ref: '#/definitions/DataExportRelatedUnmaskingWorkflow' workflow_name: type: string x-go-name: Name @@ -2840,6 +2941,46 @@ definitions: x-go-name: Message type: object x-go-package: github.com/actiontech/dms/api/dms/service/v1 + GetUnmaskingWorkflowPlaintext: + properties: + unmasking_sqls: + items: + $ref: '#/definitions/UnmaskingPlaintextSQLItem' + type: array + x-go-name: UnmaskingSQLs + view_state: + enum: + - not_activated + - active + - view_expired + - activation_expired + type: string + x-go-enum-desc: |- + not_activated UnmaskingWorkflowViewStateNotActivated + active UnmaskingWorkflowViewStateActive + view_expired UnmaskingWorkflowViewStateViewExpired + activation_expired UnmaskingWorkflowViewStateActivationExpired + x-go-name: ViewState + view_valid_until: + type: string + x-go-name: ViewValidUntil + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 + GetUnmaskingWorkflowPlaintextReply: + properties: + code: + description: code + format: int64 + type: integer + x-go-name: Code + data: + $ref: '#/definitions/GetUnmaskingWorkflowPlaintext' + message: + description: message + type: string + x-go-name: Message + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 GetUnmaskingWorkflowReply: properties: code: @@ -5595,6 +5736,24 @@ definitions: x-go-name: Message type: object x-go-package: github.com/actiontech/dms/api/dms/service/v1 + ListTableColumnsReply: + properties: + code: + description: code + format: int64 + type: integer + x-go-name: Code + data: + items: + $ref: '#/definitions/TableColumn' + type: array + x-go-name: Data + message: + description: message + type: string + x-go-name: Message + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 ListUnmaskingWorkflowsReply: properties: code: @@ -5825,6 +5984,9 @@ definitions: x-go-package: github.com/actiontech/dms/pkg/dms-common/api/dms/v1 LoginConfiguration: properties: + disable_multiple_login: + type: boolean + x-go-name: DisableMultipleLogin disable_user_pwd_login: type: boolean x-go-name: DisableUserPwdLogin @@ -5835,6 +5997,9 @@ definitions: x-go-package: github.com/actiontech/dms/api/dms/service/v1 LoginTipsResData: properties: + disable_multiple_login: + type: boolean + x-go-name: DisableMultipleLogin disable_user_pwd_login: type: boolean x-go-name: DisableUserPwdLogin @@ -7621,6 +7786,22 @@ definitions: x-go-name: Url type: object x-go-package: github.com/actiontech/dms/pkg/dms-common/api/dms/v1 + TableColumn: + properties: + comment: + type: string + x-go-name: Comment + name: + type: string + x-go-name: Name + nullable: + type: boolean + x-go-name: Nullable + type: + type: string + x-go-name: Type + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 TableColumnMaskingDetail: properties: column_name: @@ -8011,7 +8192,7 @@ definitions: type: string x-go-name: Uid type: object - x-go-package: github.com/actiontech/dms/pkg/dms-common/api/dms/v1 + x-go-package: github.com/actiontech/dms/api/dms/service/v1 UnmaskingOperationLogItem: properties: action: @@ -8024,6 +8205,7 @@ definitions: view_full_original_data UnmaskingActionViewOriginalData 查看原文 download_full_original_data UnmaskingActionDownloadOriginalData 下载原文 cancel UnmaskingActionCancel 取消申请 + activate_view UnmaskingActionActivateView 激活查看原文(工单详情) enum: - submit - approve @@ -8032,6 +8214,7 @@ definitions: - view_full_original_data - download_full_original_data - cancel + - activate_view type: string x-go-enum-desc: |- submit UnmaskingActionSubmit 提交申请 @@ -8041,6 +8224,7 @@ definitions: view_full_original_data UnmaskingActionViewOriginalData 查看原文 download_full_original_data UnmaskingActionDownloadOriginalData 下载原文 cancel UnmaskingActionCancel 取消申请 + activate_view UnmaskingActionActivateView 激活查看原文(工单详情) x-go-name: Action action_time: description: 操作时间 (RFC3339) @@ -8060,6 +8244,26 @@ definitions: x-go-name: OperatorUID type: object x-go-package: github.com/actiontech/dms/api/dms/service/v1 + UnmaskingPlaintextSQLItem: + properties: + masked_columns: + items: + type: string + type: array + x-go-name: MaskedColumns + original_data: + $ref: '#/definitions/SQLQueryResult' + sql_index_id: + type: string + x-go-name: SQLIndexID + truncated: + type: boolean + x-go-name: Truncated + uid: + type: string + x-go-name: UID + type: object + x-go-package: github.com/actiontech/dms/api/dms/service/v1 UnmaskingSQLDetail: properties: lineage_analysis_snapshot: @@ -8090,6 +8294,14 @@ definitions: x-go-package: github.com/actiontech/dms/api/dms/service/v1 UnmaskingWorkflowDetail: properties: + activated_at: + description: 激活查看时刻 (RFC3339) + type: string + x-go-name: ActivatedAt + activation_deadline: + description: 须在此时刻前激活查看 (RFC3339) + type: string + x-go-name: ActivationDeadline applicant_name: description: 申请人用户名 type: string @@ -8117,6 +8329,10 @@ definitions: rejected UnmaskingWorkflowApprovalStatusRejected 已驳回 cancelled UnmaskingWorkflowApprovalStatusCancelled 已取消 x-go-name: ApprovalStatus + can_activate: + description: 是否可点击激活查看 + type: boolean + x-go-name: CanActivate created_at: description: 申请时间 (RFC3339) type: string @@ -8136,7 +8352,7 @@ definitions: type: string x-go-name: DatasourceUid expire_time: - description: 过期时间 (RFC3339) + description: 过期时间 (RFC3339),兼容字段:批准前为激活截止,激活后为查看截止 type: string x-go-name: ExpireTime operation_logs: @@ -8185,6 +8401,29 @@ definitions: unviewed UnmaskingWorkflowUsageStatusUnviewed 未查看 viewed UnmaskingWorkflowUsageStatusViewed 已查看 x-go-name: UsageStatus + view_state: + description: |- + 申请人明文查看状态 + not_activated UnmaskingWorkflowViewStateNotActivated + active UnmaskingWorkflowViewStateActive + view_expired UnmaskingWorkflowViewStateViewExpired + activation_expired UnmaskingWorkflowViewStateActivationExpired + enum: + - not_activated + - active + - view_expired + - activation_expired + type: string + x-go-enum-desc: |- + not_activated UnmaskingWorkflowViewStateNotActivated + active UnmaskingWorkflowViewStateActive + view_expired UnmaskingWorkflowViewStateViewExpired + activation_expired UnmaskingWorkflowViewStateActivationExpired + x-go-name: ViewState + view_valid_until: + description: 明文查看截止 (RFC3339) + type: string + x-go-name: ViewValidUntil workflow_id: description: 申请编号 type: string @@ -8193,6 +8432,14 @@ definitions: x-go-package: github.com/actiontech/dms/api/dms/service/v1 UnmaskingWorkflowListItem: properties: + activated_at: + description: 激活查看时刻 (RFC3339) + type: string + x-go-name: ActivatedAt + activation_deadline: + description: 须在此时刻前激活查看 (RFC3339) + type: string + x-go-name: ActivationDeadline applicant_name: description: 申请人用户名 type: string @@ -8220,6 +8467,10 @@ definitions: rejected UnmaskingWorkflowApprovalStatusRejected 已驳回 cancelled UnmaskingWorkflowApprovalStatusCancelled 已取消 x-go-name: ApprovalStatus + can_activate: + description: 是否可点击激活查看 + type: boolean + x-go-name: CanActivate created_at: description: 申请时间 (RFC3339) type: string @@ -8239,7 +8490,7 @@ definitions: type: string x-go-name: DatasourceUid expire_time: - description: 过期时间 (RFC3339) + description: 过期时间 (RFC3339),兼容字段:批准前为激活截止,激活后为查看截止 type: string x-go-name: ExpireTime source_type: @@ -8272,6 +8523,29 @@ definitions: unviewed UnmaskingWorkflowUsageStatusUnviewed 未查看 viewed UnmaskingWorkflowUsageStatusViewed 已查看 x-go-name: UsageStatus + view_state: + description: |- + 申请人明文查看状态 + not_activated UnmaskingWorkflowViewStateNotActivated + active UnmaskingWorkflowViewStateActive + view_expired UnmaskingWorkflowViewStateViewExpired + activation_expired UnmaskingWorkflowViewStateActivationExpired + enum: + - not_activated + - active + - view_expired + - activation_expired + type: string + x-go-enum-desc: |- + not_activated UnmaskingWorkflowViewStateNotActivated + active UnmaskingWorkflowViewStateActive + view_expired UnmaskingWorkflowViewStateViewExpired + activation_expired UnmaskingWorkflowViewStateActivationExpired + x-go-name: ViewState + view_valid_until: + description: 明文查看截止 (RFC3339) + type: string + x-go-name: ViewValidUntil workflow_id: description: 申请编号 type: string @@ -10592,6 +10866,38 @@ paths: summary: Get gateway tips. tags: - Gateway + /v1/dms/masking/rules: + get: + operationId: ListMaskingRulesWithoutProject + parameters: + - description: 规则来源筛选,builtin 或 custom,为空时返回全部 + in: query + name: source + type: string + - description: 模糊搜索关键字,匹配规则名称、描述或敏感数据类型名称 + in: query + name: keywords + type: string + - description: 分页大小,默认 20 + in: query + name: page_size + type: integer + - description: 页码(从1开始),默认 1 + in: query + name: page_index + type: integer + responses: + "200": + description: 查询脱敏规则列表成功 + schema: + $ref: '#/definitions/ListMaskingRulesReply' + default: + description: 通用错误响应 + schema: + $ref: '#/definitions/GenericResp' + summary: 查询脱敏规则列表(兼容旧入口,仅返回内置规则,可携带筛选参数)。 + tags: + - Masking /v1/dms/notifications: post: operationId: Notification @@ -11547,6 +11853,37 @@ paths: summary: exec data_export workflow. tags: - DataExportWorkflows + /v1/dms/projects/{project_uid}/data_export_workflows/{data_export_workflow_uid}/original-export/download: + get: + operationId: DownloadOriginalDataExportWorkflow + parameters: + - description: project id + in: path + name: project_uid + required: true + type: string + x-go-name: ProjectUid + - in: path + name: data_export_workflow_uid + required: true + type: string + x-go-name: DataExportWorkflowUid + - description: 已批准的查看原文工单 UID + in: query + name: unmasking_workflow_uid + required: true + type: string + x-go-name: UnmaskingWorkflowUid + responses: + "200": + $ref: '#/responses/DownloadOriginalDataExportWorkflowReply' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: Download unmasked SQL query results as a zip file. Each request runs export in memory; files are not persisted. + tags: + - DataExportWorkflows /v1/dms/projects/{project_uid}/data_export_workflows/{data_export_workflow_uid}/reject: post: operationId: RejectDataExportWorkflow @@ -11816,6 +12153,42 @@ paths: summary: check if the db_service is connectable. tags: - DBService + /v1/dms/projects/{project_uid}/db_services/{db_service_uid}/schemas/{schema_name}/tables/{table_name}/columns: + get: + operationId: ListTableColumns + parameters: + - in: path + name: project_uid + required: true + type: string + x-go-name: ProjectUid + - in: path + name: db_service_uid + required: true + type: string + x-go-name: DBServiceUid + - in: path + name: schema_name + required: true + type: string + x-go-name: SchemaName + - in: path + name: table_name + required: true + type: string + x-go-name: TableName + responses: + "200": + description: ListTableColumnsReply + schema: + $ref: '#/definitions/ListTableColumnsReply' + default: + description: GenericResp + schema: + $ref: '#/definitions/GenericResp' + summary: List table columns (internal API for lineage analysis). + tags: + - DBStructure /v1/dms/projects/{project_uid}/db_services/connection: post: operationId: CheckDBServiceIsConnectable @@ -13048,6 +13421,31 @@ paths: summary: Get unmasking workflow detail. tags: - Masking + /v1/dms/projects/{project_uid}/masking/unmasking-workflows/{workflow_id}/activate: + post: + operationId: ActivateUnmaskingWorkflowView + parameters: + - in: path + name: project_uid + required: true + type: string + x-go-name: WorkflowID + - in: path + name: workflow_id + required: true + type: string + responses: + "200": + description: "" + schema: + $ref: '#/definitions/ActivateUnmaskingWorkflowViewReply' + default: + description: "" + schema: + $ref: '#/definitions/GenericResp' + summary: Activate plaintext view window for applicant. + tags: + - Masking /v1/dms/projects/{project_uid}/masking/unmasking-workflows/{workflow_id}/approve: post: operationId: ApproveUnmaskingWorkflow @@ -13107,6 +13505,31 @@ paths: summary: Cancel unmasking workflow. tags: - Masking + /v1/dms/projects/{project_uid}/masking/unmasking-workflows/{workflow_id}/plaintext: + get: + operationId: GetUnmaskingWorkflowPlaintext + parameters: + - in: path + name: project_uid + required: true + type: string + x-go-name: WorkflowID + - in: path + name: workflow_id + required: true + type: string + responses: + "200": + description: "" + schema: + $ref: '#/definitions/GetUnmaskingWorkflowPlaintextReply' + default: + description: "" + schema: + $ref: '#/definitions/GenericResp' + summary: Get plaintext query snapshot during active view window. + tags: + - Masking /v1/dms/projects/{project_uid}/masking/unmasking-workflows/{workflow_id}/reject: post: operationId: RejectUnmaskingWorkflow @@ -15326,6 +15749,10 @@ responses: description: "" schema: type: file + DownloadOriginalDataExportWorkflowReply: + description: "" + schema: + type: file DownloadResourceOverviewListRes: description: "" schema: diff --git a/go.mod b/go.mod index 997803096..85586790f 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/labstack/echo-jwt/v4 v4.1.0 github.com/nicksnyder/go-i18n/v2 v2.4.0 + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/robfig/cron/v3 v3.0.1 github.com/stretchr/testify v1.11.1 github.com/swaggo/echo-swagger v1.4.1 diff --git a/go.sum b/go.sum index eea7d6c62..d3de74b32 100644 --- a/go.sum +++ b/go.sum @@ -630,6 +630,8 @@ github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqi github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= diff --git a/internal/apiserver/service/dms_controller.go b/internal/apiserver/service/dms_controller.go index 95f773fa6..e8c0bcd17 100644 --- a/internal/apiserver/service/dms_controller.go +++ b/internal/apiserver/service/dms_controller.go @@ -693,12 +693,17 @@ func (ctl *DMSController) AddSession(c echo.Context) error { return NewErrResp(c, errors.New(reply.Data.VerifyFailedMsg), apiError.BadRequestErr) } + loginSessionID, err := ctl.DMS.UserUsecase.CreateLoginSession(c.Request().Context(), reply.Data.UserUid) + if nil != err { + return NewErrResp(c, err, apiError.APIServerErr) + } + // Create token with claims - token, err := jwt.GenJwtToken(jwt.WithUserId(reply.Data.UserUid)) + token, err := jwt.GenJwtToken(jwt.WithUserId(reply.Data.UserUid), jwt.WithLoginSessionID(loginSessionID)) if nil != err { return NewErrResp(c, err, apiError.APIServerErr) } - refreshToken, err := jwt.GenRefreshToken(jwt.WithUserId(reply.Data.UserUid)) + refreshToken, err := jwt.GenRefreshToken(jwt.WithUserId(reply.Data.UserUid), jwt.WithLoginSessionID(loginSessionID)) if nil != err { return NewErrResp(c, err, apiError.APIServerErr) } @@ -873,12 +878,21 @@ func (ctl *DMSController) RefreshSession(c echo.Context) error { return c.String(http.StatusUnauthorized, "refresh token is expired") } + loginSessionID, err := jwt.ParseLoginSessionIDFromRefreshToken(refreshToken.Value) + if err != nil { + return c.String(http.StatusUnauthorized, err.Error()) + } + if err = ctl.DMS.UserUsecase.ValidateLoginSession(c.Request().Context(), uid, loginSessionID); err != nil { + return c.String(http.StatusUnauthorized, err.Error()) + } + // 签发的token包含第三方平台信息,需要同步刷新第三方平台token if sub != "" || sid != "" { claims, err := ctl.DMS.RefreshOauth2Token(c.Request().Context(), uid, sub, sid) if err != nil { return c.String(http.StatusUnauthorized, err.Error()) } + claims.LoginSessionID = loginSessionID newDmsToken, dmsCookieExp, err := claims.DmsToken() if err != nil { @@ -915,7 +929,7 @@ func (ctl *DMSController) RefreshSession(c echo.Context) error { } // Create token with claims - token, err := jwt.GenJwtToken(jwt.WithUserId(uid)) + token, err := jwt.GenJwtToken(jwt.WithUserId(uid), jwt.WithLoginSessionID(loginSessionID)) if nil != err { return NewErrResp(c, err, apiError.APIServerErr) } @@ -2746,6 +2760,12 @@ func (ctl *DMSController) oauth2Callback(c echo.Context) error { // 2. callbackData.UserExist 为false时,前端会进入手动绑定页面,绑定时调用绑定接口签发tokens // 3. 没错误且用户存在时,签发tokens登录成功 if callbackData.Error == "" && callbackData.UserExist { + loginSessionID, err := ctl.DMS.UserUsecase.CreateLoginSession(c.Request().Context(), claims.UserId) + if err != nil { + return NewErrResp(c, err, apiError.APIServerErr) + } + claims.LoginSessionID = loginSessionID + dmsToken, dmsCookieExp, err := claims.DmsToken() if err != nil { return NewErrResp(c, err, apiError.APIServerErr) @@ -2832,6 +2852,12 @@ func (ctl *DMSController) BindOauth2User(c echo.Context) error { return NewErrResp(c, err, apiError.APIServerErr) } + loginSessionID, err := ctl.DMS.UserUsecase.CreateLoginSession(c.Request().Context(), claims.UserId) + if err != nil { + return NewErrResp(c, err, apiError.APIServerErr) + } + claims.LoginSessionID = loginSessionID + dmsToken, dmsCookieExp, err := claims.DmsToken() if err != nil { return NewErrResp(c, err, apiError.APIServerErr) diff --git a/internal/apiserver/service/router.go b/internal/apiserver/service/router.go index 4d85a029d..1d285d250 100644 --- a/internal/apiserver/service/router.go +++ b/internal/apiserver/service/router.go @@ -409,6 +409,7 @@ func (s *APIServer) installMiddleware() error { SigningKey: dmsV1.JwtSigningKey, TokenLookup: "cookie:dms-token,header:Authorization:Bearer ", // tell the middleware where to get token: from cookie and header, })) + s.echo.Use(s.DMSController.DMS.AuthLoginSessionUsecase.CheckSingleActiveSession(biz.GatewayForwardedHeader)) s.echo.Use(s.DMSController.DMS.Oauth2ConfigurationUsecase.CheckBackChannelLogoutEvent()) // middleware gateway s.echo.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ diff --git a/internal/dms/biz/gateway.go b/internal/dms/biz/gateway.go index c36e55a23..eaf7733a3 100644 --- a/internal/dms/biz/gateway.go +++ b/internal/dms/biz/gateway.go @@ -6,6 +6,8 @@ import ( pkgConst "github.com/actiontech/dms/internal/dms/pkg/constant" ) +const GatewayForwardedHeader = "X-Gateway-Forwarded" + type GatewayRepo interface { AddGateway(ctx context.Context, u *Gateway) error DeleteGateway(ctx context.Context, id string) error diff --git a/internal/dms/biz/login_configuration.go b/internal/dms/biz/login_configuration.go index 4f836d68b..04a9f3f8b 100644 --- a/internal/dms/biz/login_configuration.go +++ b/internal/dms/biz/login_configuration.go @@ -2,16 +2,19 @@ package biz import ( "context" + "time" utilLog "github.com/actiontech/dms/pkg/dms-common/pkg/log" pkgRand "github.com/actiontech/dms/pkg/rand" + "github.com/patrickmn/go-cache" ) type LoginConfiguration struct { Base - UID string - LoginButtonText string - DisableUserPwdLogin bool + UID string + LoginButtonText string + DisableUserPwdLogin bool + DisableMultipleLogin bool } func defaultLoginConfiguration() (*LoginConfiguration, error) { @@ -20,9 +23,10 @@ func defaultLoginConfiguration() (*LoginConfiguration, error) { return nil, err } return &LoginConfiguration{ - UID: uid, - LoginButtonText: "登录", - DisableUserPwdLogin: false, + UID: uid, + LoginButtonText: "登录", + DisableUserPwdLogin: false, + DisableMultipleLogin: false, }, nil } @@ -32,15 +36,35 @@ type LoginConfigurationRepo interface { } type LoginConfigurationUsecase struct { - tx TransactionGenerator - repo LoginConfigurationRepo - log *utilLog.Helper + tx TransactionGenerator + repo LoginConfigurationRepo + log *utilLog.Helper + disableMultipleLoginCache *cache.Cache } func NewLoginConfigurationUsecase(log utilLog.Logger, tx TransactionGenerator, repo LoginConfigurationRepo) *LoginConfigurationUsecase { return &LoginConfigurationUsecase{ - tx: tx, - repo: repo, - log: utilLog.NewHelper(log, utilLog.WithMessageKey("biz.Login_configuration")), + tx: tx, + repo: repo, + log: utilLog.NewHelper(log, utilLog.WithMessageKey("biz.Login_configuration")), + disableMultipleLoginCache: cache.New(10*time.Second, 30*time.Second), } } + +func (d *LoginConfigurationUsecase) IsDisableMultipleLogin(ctx context.Context) (bool, error) { + if cached, found := d.disableMultipleLoginCache.Get("disable_multiple_login"); found { + if val, ok := cached.(bool); ok { + return val, nil + } + } + loginC, err := d.GetLoginConfiguration(ctx) + if err != nil { + return false, err + } + d.disableMultipleLoginCache.Set("disable_multiple_login", loginC.DisableMultipleLogin, cache.DefaultExpiration) + return loginC.DisableMultipleLogin, nil +} + +func (d *LoginConfigurationUsecase) invalidateDisableMultipleLoginCache() { + d.disableMultipleLoginCache.Delete("disable_multiple_login") +} diff --git a/internal/dms/biz/login_configuration_ce.go b/internal/dms/biz/login_configuration_ce.go index f1a3fa841..8c8a7db7b 100644 --- a/internal/dms/biz/login_configuration_ce.go +++ b/internal/dms/biz/login_configuration_ce.go @@ -9,13 +9,14 @@ import ( var errNotSupportLoginConfiguration = errors.New("login configuration related functions are enterprise version functions") -func (d *LoginConfigurationUsecase) UpdateLoginConfiguration(ctx context.Context, LoginButtonText *string, DisableUserPwdLogin *bool) error { +func (d *LoginConfigurationUsecase) UpdateLoginConfiguration(ctx context.Context, LoginButtonText *string, DisableUserPwdLogin *bool, DisableMultipleLogin *bool) error { return errNotSupportLoginConfiguration } func (d *LoginConfigurationUsecase) GetLoginConfiguration(ctx context.Context) (loginC *LoginConfiguration, err error) { return &LoginConfiguration{ - LoginButtonText: "登录", - DisableUserPwdLogin: false, + LoginButtonText: "登录", + DisableUserPwdLogin: false, + DisableMultipleLogin: false, }, nil } diff --git a/internal/dms/biz/login_session.go b/internal/dms/biz/login_session.go new file mode 100644 index 000000000..ad3dd90aa --- /dev/null +++ b/internal/dms/biz/login_session.go @@ -0,0 +1,101 @@ +package biz + +import ( + "context" + "fmt" + "net/http" + + jwtPkg "github.com/actiontech/dms/pkg/dms-common/api/jwt" + utilLog "github.com/actiontech/dms/pkg/dms-common/pkg/log" + pkgRand "github.com/actiontech/dms/pkg/rand" + + "github.com/labstack/echo/v4" +) + +type AuthLoginSessionUsecase struct { + userUsecase *UserUsecase + loginConfigurationUsecase *LoginConfigurationUsecase + log *utilLog.Helper +} + +func NewAuthLoginSessionUsecase(log utilLog.Logger, userUsecase *UserUsecase, loginConfigurationUsecase *LoginConfigurationUsecase) *AuthLoginSessionUsecase { + return &AuthLoginSessionUsecase{ + userUsecase: userUsecase, + loginConfigurationUsecase: loginConfigurationUsecase, + log: utilLog.NewHelper(log, utilLog.WithMessageKey("biz.login_session")), + } +} + +func (au *AuthLoginSessionUsecase) CheckSingleActiveSession(gatewayForwardedHeader string) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if c.Request().Header.Get(gatewayForwardedHeader) != "" { + return next(c) + } + + tokenDetail, err := jwtPkg.GetTokenDetailFromContext(c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("get token detail failed, err:%v", err)) + } + if tokenDetail.UID == "" { + return next(c) + } + if tokenDetail.LoginType == AccessTokenLogin { + return next(c) + } + + disableMultipleLogin, err := au.loginConfigurationUsecase.IsDisableMultipleLogin(c.Request().Context()) + if err != nil { + return err + } + if !disableMultipleLogin { + return next(c) + } + if tokenDetail.LoginSessionID == "" { + return echo.NewHTTPError(http.StatusUnauthorized, "login session is invalid, please login again") + } + + latestSessionID, exists, err := au.userUsecase.GetLatestLoginSession(c.Request().Context(), tokenDetail.UID) + if err != nil { + return err + } + if !exists || latestSessionID != tokenDetail.LoginSessionID { + return echo.NewHTTPError(http.StatusUnauthorized, "account has been logged in on another device") + } + + return next(c) + } + } +} + +func (d *UserUsecase) CreateLoginSession(ctx context.Context, userUID string) (sessionID string, err error) { + sessionID, err = pkgRand.GenStrUid() + if err != nil { + return "", err + } + if err = d.RecordLoginSession(ctx, userUID, sessionID); err != nil { + return "", err + } + return sessionID, nil +} + +func (d *UserUsecase) ValidateLoginSession(ctx context.Context, userUID, sessionID string) error { + disableMultipleLogin, err := d.loginConfigurationUsecase.IsDisableMultipleLogin(ctx) + if err != nil { + return err + } + if !disableMultipleLogin { + return nil + } + if sessionID == "" { + return fmt.Errorf("login session is invalid, please login again") + } + latestSessionID, exists, err := d.GetLatestLoginSession(ctx, userUID) + if err != nil { + return err + } + if !exists || latestSessionID != sessionID { + return fmt.Errorf("account has been logged in on another device") + } + return nil +} diff --git a/internal/dms/biz/oauth2_configuration.go b/internal/dms/biz/oauth2_configuration.go index 2c77bbb05..40a50c32d 100644 --- a/internal/dms/biz/oauth2_configuration.go +++ b/internal/dms/biz/oauth2_configuration.go @@ -139,20 +139,25 @@ func (c CallbackRedirectData) Generate() string { } type ClaimsInfo struct { - UserId string `json:"user_id"` // dms用户ID - Iat float64 `json:"iat"` // 第三方AccessToken 签发时间 (Issued At),Unix 时间戳 - Exp float64 `json:"exp"` // 第三方AccessToken 过期时间 (Expiration Time),Unix 时间戳 - Sub string `json:"sub"` // 第三方AccessToken 主题 (Subject),通常是用户ID或唯一标识符 - Sid string `json:"sid"` // 第三方AccessToken 会话ID (Session ID),用于跟踪用户会话 - RefreshIat float64 `json:"refresh_iat"` // 第三方RefreshToken 签发时间 (Issued At),Unix 时间戳 - RefreshExp float64 `json:"refresh_exp"` // 第三方RefreshToken 过期时间 (Expiration Time),Unix 时间戳 + UserId string `json:"user_id"` // dms用户ID + Iat float64 `json:"iat"` // 第三方AccessToken 签发时间 (Issued At),Unix 时间戳 + Exp float64 `json:"exp"` // 第三方AccessToken 过期时间 (Expiration Time),Unix 时间戳 + Sub string `json:"sub"` // 第三方AccessToken 主题 (Subject),通常是用户ID或唯一标识符 + Sid string `json:"sid"` // 第三方AccessToken 会话ID (Session ID),用于跟踪用户会话 + RefreshIat float64 `json:"refresh_iat"` // 第三方RefreshToken 签发时间 (Issued At),Unix 时间戳 + RefreshExp float64 `json:"refresh_exp"` // 第三方RefreshToken 过期时间 (Expiration Time),Unix 时间戳 + LoginSessionID string `json:"login_session_id"` } func (c *ClaimsInfo) DmsToken() (token string, cookieExp time.Duration, err error) { c.setDefaults() // 为了在第三方会话“快过期”时去刷新第三方token,故此时(通过OAuth2登录)签发的DmsToken有效期为第三方平台的0.9 cookieExp = time.Duration((c.Exp-c.Iat)*0.9) * time.Second - token, err = jwt.GenJwtToken(jwt.WithUserId(c.UserId), jwt.WithExpiredTime(cookieExp), jwt.WithSub(c.Sub), jwt.WithSid(c.Sid)) + customClaims := []jwt.CustomClaimFunc{jwt.WithUserId(c.UserId), jwt.WithExpiredTime(cookieExp), jwt.WithSub(c.Sub), jwt.WithSid(c.Sid)} + if c.LoginSessionID != "" { + customClaims = append(customClaims, jwt.WithLoginSessionID(c.LoginSessionID)) + } + token, err = jwt.GenJwtToken(customClaims...) return } @@ -161,7 +166,11 @@ func (c *ClaimsInfo) DmsRefreshToken() (token string, cookieExp time.Duration, e // cookie有效期更久,和第三方refresh token有效期保持一致 // 这样在DmsRefreshToken过期时,cookie仍可获取,用于注销第三方会话 cookieExp = time.Duration(c.RefreshExp-c.RefreshIat) * time.Second - token, err = jwt.GenRefreshToken(jwt.WithUserId(c.UserId), jwt.WithExpiredTime(time.Duration(c.Exp-c.Iat)*time.Second), jwt.WithSub(c.Sub), jwt.WithSid(c.Sid)) + customClaims := []jwt.CustomClaimFunc{jwt.WithUserId(c.UserId), jwt.WithExpiredTime(time.Duration(c.Exp-c.Iat)*time.Second), jwt.WithSub(c.Sub), jwt.WithSid(c.Sid)} + if c.LoginSessionID != "" { + customClaims = append(customClaims, jwt.WithLoginSessionID(c.LoginSessionID)) + } + token, err = jwt.GenRefreshToken(customClaims...) return } diff --git a/internal/dms/biz/op_permission_verify_test.go b/internal/dms/biz/op_permission_verify_test.go index 41c7dfb8a..c48f155dd 100644 --- a/internal/dms/biz/op_permission_verify_test.go +++ b/internal/dms/biz/op_permission_verify_test.go @@ -55,6 +55,10 @@ func (m *mockUserRepo) SaveAccessToken(context.Context, *AccessTokenInfo) error func (m *mockUserRepo) GetAccessTokenByUser(context.Context, string) (*AccessTokenInfo, error) { return nil, nil } +func (m *mockUserRepo) RecordLoginSession(context.Context, string, string) error { return nil } +func (m *mockUserRepo) GetLatestLoginSession(context.Context, string) (string, bool, error) { + return "", false, nil +} // mockOpPermissionVerifyRepo implements OpPermissionVerifyRepo for testing type mockOpPermissionVerifyRepo struct { diff --git a/internal/dms/biz/user.go b/internal/dms/biz/user.go index e5bdfe49c..e05be0d53 100644 --- a/internal/dms/biz/user.go +++ b/internal/dms/biz/user.go @@ -202,6 +202,8 @@ type UserRepo interface { GetUserByThirdPartyUserID(ctx context.Context, thirdPartyUserUID string) (*User, error) SaveAccessToken(ctx context.Context, accessTokenInfo *AccessTokenInfo) error GetAccessTokenByUser(ctx context.Context, UserUid string) (*AccessTokenInfo, error) + RecordLoginSession(ctx context.Context, userUID, sessionID string) error + GetLatestLoginSession(ctx context.Context, userUID string) (sessionID string, exists bool, err error) } // SqlWorkbenchUser SqlWorkbench用户缓存 @@ -1068,6 +1070,14 @@ func (d *UserUsecase) GetAccessTokenByUser(ctx context.Context, UserUid string) return accessTokenInfo, nil } +func (d *UserUsecase) RecordLoginSession(ctx context.Context, userUID, sessionID string) error { + return d.repo.RecordLoginSession(ctx, userUID, sessionID) +} + +func (d *UserUsecase) GetLatestLoginSession(ctx context.Context, userUID string) (sessionID string, exists bool, err error) { + return d.repo.GetLatestLoginSession(ctx, userUID) +} + func (d *UserUsecase) GetUserLanguageByEchoCtx(c echo.Context) string { uid, err := jwt.GetUserUidStrFromContext(c) if err != nil { diff --git a/internal/dms/service/configuration.go b/internal/dms/service/configuration.go index 2d4140ddd..6b5c53f9f 100644 --- a/internal/dms/service/configuration.go +++ b/internal/dms/service/configuration.go @@ -23,8 +23,9 @@ func (d *DMSService) GetLoginTips(ctx context.Context) (reply *dmsV1.GetLoginTip return &dmsV1.GetLoginTipsReply{ Data: dmsV1.LoginTipsResData{ - LoginButtonText: loginConfiguration.LoginButtonText, - DisableUserPwdLogin: loginConfiguration.DisableUserPwdLogin, + LoginButtonText: loginConfiguration.LoginButtonText, + DisableUserPwdLogin: loginConfiguration.DisableUserPwdLogin, + DisableMultipleLogin: loginConfiguration.DisableMultipleLogin, }, }, nil } @@ -43,7 +44,7 @@ func (d *DMSService) UpdateLoginConfiguration(ctx context.Context, userId string } loginConfiguration := req.LoginConfiguration - err = d.LoginConfigurationUsecase.UpdateLoginConfiguration(ctx, loginConfiguration.LoginButtonText, loginConfiguration.DisableUserPwdLogin) + err = d.LoginConfigurationUsecase.UpdateLoginConfiguration(ctx, loginConfiguration.LoginButtonText, loginConfiguration.DisableUserPwdLogin, loginConfiguration.DisableMultipleLogin) return } diff --git a/internal/dms/service/service.go b/internal/dms/service/service.go index f88b4ac40..73a1b8063 100644 --- a/internal/dms/service/service.go +++ b/internal/dms/service/service.go @@ -46,6 +46,7 @@ type DMSService struct { DataMaskingUsecase *dataMaskingUsecase FunctionSupportRegistry *biz.FunctionSupportRegistry AuthAccessTokenUseCase *biz.AuthAccessTokenUsecase + AuthLoginSessionUsecase *biz.AuthLoginSessionUsecase SwaggerUseCase *biz.SwaggerUseCase GatewayUsecase *biz.GatewayUsecase SystemVariableUsecase *biz.SystemVariableUsecase @@ -170,6 +171,7 @@ func NewAndInitDMSService(logger utilLog.Logger, opts *conf.DMSOptions) (*DMSSer registerFunctionProvidersToRegistry(functionSupportRegistry, dataMaskingUsecase) authAccessTokenUsecase := biz.NewAuthAccessTokenUsecase(logger, userUsecase) + authLoginSessionUsecase := biz.NewAuthLoginSessionUsecase(logger, userUsecase, loginConfigurationUsecase) cronTask := biz.NewCronTaskUsecase(logger, DataExportWorkflowUsecase, CbOperationLogUsecase, operationRecordUsecase, oauth2SessionUsecase) err = cronTask.InitialTask() @@ -212,6 +214,7 @@ func NewAndInitDMSService(logger utilLog.Logger, opts *conf.DMSOptions) (*DMSSer DataMaskingUsecase: dataMaskingUsecase, FunctionSupportRegistry: functionSupportRegistry, AuthAccessTokenUseCase: authAccessTokenUsecase, + AuthLoginSessionUsecase: authLoginSessionUsecase, SwaggerUseCase: swaggerUseCase, GatewayUsecase: gatewayUsecase, SystemVariableUsecase: systemVariableUsecase, diff --git a/internal/dms/storage/convert.go b/internal/dms/storage/convert.go index 5637fba89..6220c1e80 100644 --- a/internal/dms/storage/convert.go +++ b/internal/dms/storage/convert.go @@ -698,17 +698,19 @@ func convertBizLoginConfiguration(b *biz.LoginConfiguration) (*model.LoginConfig Model: model.Model{ UID: b.UID, }, - LoginButtonText: b.LoginButtonText, - DisableUserPwdLogin: b.DisableUserPwdLogin, + LoginButtonText: b.LoginButtonText, + DisableUserPwdLogin: b.DisableUserPwdLogin, + DisableMultipleLogin: b.DisableMultipleLogin, }, nil } func convertModelLoginConfiguration(m *model.LoginConfiguration) (*biz.LoginConfiguration, error) { return &biz.LoginConfiguration{ - Base: convertBase(m.Model), - UID: m.UID, - LoginButtonText: m.LoginButtonText, - DisableUserPwdLogin: m.DisableUserPwdLogin, + Base: convertBase(m.Model), + UID: m.UID, + LoginButtonText: m.LoginButtonText, + DisableUserPwdLogin: m.DisableUserPwdLogin, + DisableMultipleLogin: m.DisableMultipleLogin, }, nil } diff --git a/internal/dms/storage/model/model.go b/internal/dms/storage/model/model.go index 9e877d636..78de8e6bb 100644 --- a/internal/dms/storage/model/model.go +++ b/internal/dms/storage/model/model.go @@ -55,6 +55,7 @@ var AutoMigrateList = []interface{}{ DataExportTask{}, DataExportTaskRecord{}, UserAccessToken{}, + UserLoginSession{}, CbOperationLog{}, EnvironmentTag{}, Gateway{}, @@ -295,8 +296,16 @@ func (OAuth2Session) TableName() string { // LoginConfiguration store local login configuration. type LoginConfiguration struct { Model - LoginButtonText string `json:"login_button_text" gorm:"column:login_button_text;size:255;default:'登录';not null"` - DisableUserPwdLogin bool `json:"disable_user_pwd_login" gorm:"column:disable_user_pwd_login;default:false;not null"` + LoginButtonText string `json:"login_button_text" gorm:"column:login_button_text;size:255;default:'登录';not null"` + DisableUserPwdLogin bool `json:"disable_user_pwd_login" gorm:"column:disable_user_pwd_login;default:false;not null"` + DisableMultipleLogin bool `json:"disable_multiple_login" gorm:"column:disable_multiple_login;default:false;not null"` +} + +// UserLoginSession stores the latest active login session ID per user. +type UserLoginSession struct { + Model + UserUID string `json:"user_uid" gorm:"size:32;column:user_uid;uniqueIndex"` + SessionID string `json:"session_id" gorm:"size:32;column:session_id;not null"` } // Oauth2Configuration store oauth2 server configuration. diff --git a/internal/dms/storage/user.go b/internal/dms/storage/user.go index e91897814..65f16566b 100644 --- a/internal/dms/storage/user.go +++ b/internal/dms/storage/user.go @@ -11,6 +11,7 @@ import ( "github.com/actiontech/dms/internal/dms/storage/model" utilLog "github.com/actiontech/dms/pkg/dms-common/pkg/log" + pkgRand "github.com/actiontech/dms/pkg/rand" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -391,3 +392,36 @@ func (d *UserRepo) GetAccessTokenByUser(ctx context.Context, userUid string) (*b return &biz.AccessTokenInfo{Token: userToken.Token, ExpiredTime: userToken.ExpiredTime}, nil } + +func (d *UserRepo) RecordLoginSession(ctx context.Context, userUID, sessionID string) error { + uid, err := pkgRand.GenStrUid() + if err != nil { + return fmt.Errorf("failed to generate login session uid: %v", err) + } + loginSession := &model.UserLoginSession{ + Model: model.Model{ + UID: uid, + }, + UserUID: userUID, + SessionID: sessionID, + } + tx := d.db.WithContext(ctx).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "user_uid"}}, + DoUpdates: clause.AssignmentColumns([]string{"session_id", "updated_at"}), + }).Create(loginSession) + if tx.Error != nil { + return fmt.Errorf("failed to record login session: %v", tx.Error) + } + return nil +} + +func (d *UserRepo) GetLatestLoginSession(ctx context.Context, userUID string) (sessionID string, exists bool, err error) { + var loginSession model.UserLoginSession + if err := d.db.WithContext(ctx).First(&loginSession, "user_uid = ?", userUID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", false, nil + } + return "", false, fmt.Errorf("failed to get login session: %v", err) + } + return loginSession.SessionID, true, nil +} diff --git a/pkg/dms-common/api/jwt/jwt.go b/pkg/dms-common/api/jwt/jwt.go index e72e39aeb..4dba14425 100644 --- a/pkg/dms-common/api/jwt/jwt.go +++ b/pkg/dms-common/api/jwt/jwt.go @@ -21,12 +21,13 @@ type EchoContextGetter interface { type CustomClaimFunc func(claims jwt.MapClaims) const ( - JWTUserId = "uid" - JWTUsername = "name" - JWTExpiredTime = "exp" - JWTAuditPlanName = "apn" - JWTLoginType = "loginType" - JWTType = "typ" + JWTUserId = "uid" + JWTUsername = "name" + JWTExpiredTime = "exp" + JWTAuditPlanName = "apn" + JWTLoginType = "loginType" + JWTType = "typ" + JWTLoginSessionID = "jti" DefaultDmsTokenExpHours = 2 DefaultDmsRefreshTokenExpHours = 24 @@ -90,6 +91,22 @@ func ParseRefreshToken(tokenStr string) (userUid, sub, sid string, expired bool, return userUid, sub, sid, expired, nil } +func ParseLoginSessionIDFromRefreshToken(tokenStr string) (loginSessionID string, err error) { + token, err := parseJwtTokenStr(tokenStr) + if err != nil { + return "", err + } + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return "", fmt.Errorf("failed to convert token claims to jwt") + } + if fmt.Sprint(claims[JWTType]) != constant.DMSRefreshToken { + return "", fmt.Errorf("invalid jwt type") + } + loginSessionID, _ = claims[JWTLoginSessionID].(string) + return loginSessionID, nil +} + func genJwtToken(mapClaims jwt.MapClaims, customClaims ...CustomClaimFunc) (tokenStr string, err error) { for _, claimFunc := range customClaims { claimFunc(mapClaims) @@ -154,6 +171,12 @@ func WithSid(sid string) CustomClaimFunc { } } +func WithLoginSessionID(id string) CustomClaimFunc { + return func(claims jwt.MapClaims) { + claims[JWTLoginSessionID] = id + } +} + func ParseUidFromJwtTokenStr(tokenStr string) (uid string, err error) { token, err := parseJwtTokenStr(tokenStr) if err != nil { @@ -314,9 +337,10 @@ func ParseUserUidStrFromTokenWithOldJwt(token *jwtOld.Token) (uid string, err er } type TokenDetail struct { - TokenStr string - UID string - LoginType string + TokenStr string + UID string + LoginType string + LoginSessionID string } // 由于sqle使用的github.com/golang-jwt/jwt,本方法为sqle兼容 @@ -348,10 +372,16 @@ func GetTokenDetailFromContextWithOldJwt(c EchoContextGetter) (tokenDetail *Toke } loginType, ok := claims[JWTLoginType] if !ok { + if loginSessionID, ok := claims[JWTLoginSessionID]; ok { + tokenDetail.LoginSessionID = fmt.Sprint(loginSessionID) + } return tokenDetail, nil } tokenDetail.LoginType = fmt.Sprint(loginType) + if loginSessionID, ok := claims[JWTLoginSessionID]; ok { + tokenDetail.LoginSessionID = fmt.Sprint(loginSessionID) + } return tokenDetail, nil } @@ -382,9 +412,15 @@ func GetTokenDetailFromContext(c EchoContextGetter) (tokenDetail *TokenDetail, e } loginType, ok := claims[JWTLoginType] if !ok { + if loginSessionID, ok := claims[JWTLoginSessionID]; ok { + tokenDetail.LoginSessionID = fmt.Sprint(loginSessionID) + } return tokenDetail, nil } tokenDetail.LoginType = fmt.Sprint(loginType) + if loginSessionID, ok := claims[JWTLoginSessionID]; ok { + tokenDetail.LoginSessionID = fmt.Sprint(loginSessionID) + } return tokenDetail, nil } diff --git a/vendor/github.com/patrickmn/go-cache/CONTRIBUTORS b/vendor/github.com/patrickmn/go-cache/CONTRIBUTORS new file mode 100644 index 000000000..2b16e9974 --- /dev/null +++ b/vendor/github.com/patrickmn/go-cache/CONTRIBUTORS @@ -0,0 +1,9 @@ +This is a list of people who have contributed code to go-cache. They, or their +employers, are the copyright holders of the contributed code. Contributed code +is subject to the license restrictions listed in LICENSE (as they were when the +code was contributed.) + +Dustin Sallings +Jason Mooberry +Sergey Shepelev +Alex Edwards diff --git a/vendor/github.com/patrickmn/go-cache/LICENSE b/vendor/github.com/patrickmn/go-cache/LICENSE new file mode 100644 index 000000000..db9903c75 --- /dev/null +++ b/vendor/github.com/patrickmn/go-cache/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012-2017 Patrick Mylund Nielsen and the go-cache contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/patrickmn/go-cache/README.md b/vendor/github.com/patrickmn/go-cache/README.md new file mode 100644 index 000000000..c5789cc66 --- /dev/null +++ b/vendor/github.com/patrickmn/go-cache/README.md @@ -0,0 +1,83 @@ +# go-cache + +go-cache is an in-memory key:value store/cache similar to memcached that is +suitable for applications running on a single machine. Its major advantage is +that, being essentially a thread-safe `map[string]interface{}` with expiration +times, it doesn't need to serialize or transmit its contents over the network. + +Any object can be stored, for a given duration or forever, and the cache can be +safely used by multiple goroutines. + +Although go-cache isn't meant to be used as a persistent datastore, the entire +cache can be saved to and loaded from a file (using `c.Items()` to retrieve the +items map to serialize, and `NewFrom()` to create a cache from a deserialized +one) to recover from downtime quickly. (See the docs for `NewFrom()` for caveats.) + +### Installation + +`go get github.com/patrickmn/go-cache` + +### Usage + +```go +import ( + "fmt" + "github.com/patrickmn/go-cache" + "time" +) + +func main() { + // Create a cache with a default expiration time of 5 minutes, and which + // purges expired items every 10 minutes + c := cache.New(5*time.Minute, 10*time.Minute) + + // Set the value of the key "foo" to "bar", with the default expiration time + c.Set("foo", "bar", cache.DefaultExpiration) + + // Set the value of the key "baz" to 42, with no expiration time + // (the item won't be removed until it is re-set, or removed using + // c.Delete("baz") + c.Set("baz", 42, cache.NoExpiration) + + // Get the string associated with the key "foo" from the cache + foo, found := c.Get("foo") + if found { + fmt.Println(foo) + } + + // Since Go is statically typed, and cache values can be anything, type + // assertion is needed when values are being passed to functions that don't + // take arbitrary types, (i.e. interface{}). The simplest way to do this for + // values which will only be used once--e.g. for passing to another + // function--is: + foo, found := c.Get("foo") + if found { + MyFunction(foo.(string)) + } + + // This gets tedious if the value is used several times in the same function. + // You might do either of the following instead: + if x, found := c.Get("foo"); found { + foo := x.(string) + // ... + } + // or + var foo string + if x, found := c.Get("foo"); found { + foo = x.(string) + } + // ... + // foo can then be passed around freely as a string + + // Want performance? Store pointers! + c.Set("foo", &MyStruct, cache.DefaultExpiration) + if x, found := c.Get("foo"); found { + foo := x.(*MyStruct) + // ... + } +} +``` + +### Reference + +`godoc` or [http://godoc.org/github.com/patrickmn/go-cache](http://godoc.org/github.com/patrickmn/go-cache) diff --git a/vendor/github.com/patrickmn/go-cache/cache.go b/vendor/github.com/patrickmn/go-cache/cache.go new file mode 100644 index 000000000..db88d2f2c --- /dev/null +++ b/vendor/github.com/patrickmn/go-cache/cache.go @@ -0,0 +1,1161 @@ +package cache + +import ( + "encoding/gob" + "fmt" + "io" + "os" + "runtime" + "sync" + "time" +) + +type Item struct { + Object interface{} + Expiration int64 +} + +// Returns true if the item has expired. +func (item Item) Expired() bool { + if item.Expiration == 0 { + return false + } + return time.Now().UnixNano() > item.Expiration +} + +const ( + // For use with functions that take an expiration time. + NoExpiration time.Duration = -1 + // For use with functions that take an expiration time. Equivalent to + // passing in the same expiration duration as was given to New() or + // NewFrom() when the cache was created (e.g. 5 minutes.) + DefaultExpiration time.Duration = 0 +) + +type Cache struct { + *cache + // If this is confusing, see the comment at the bottom of New() +} + +type cache struct { + defaultExpiration time.Duration + items map[string]Item + mu sync.RWMutex + onEvicted func(string, interface{}) + janitor *janitor +} + +// Add an item to the cache, replacing any existing item. If the duration is 0 +// (DefaultExpiration), the cache's default expiration time is used. If it is -1 +// (NoExpiration), the item never expires. +func (c *cache) Set(k string, x interface{}, d time.Duration) { + // "Inlining" of set + var e int64 + if d == DefaultExpiration { + d = c.defaultExpiration + } + if d > 0 { + e = time.Now().Add(d).UnixNano() + } + c.mu.Lock() + c.items[k] = Item{ + Object: x, + Expiration: e, + } + // TODO: Calls to mu.Unlock are currently not deferred because defer + // adds ~200 ns (as of go1.) + c.mu.Unlock() +} + +func (c *cache) set(k string, x interface{}, d time.Duration) { + var e int64 + if d == DefaultExpiration { + d = c.defaultExpiration + } + if d > 0 { + e = time.Now().Add(d).UnixNano() + } + c.items[k] = Item{ + Object: x, + Expiration: e, + } +} + +// Add an item to the cache, replacing any existing item, using the default +// expiration. +func (c *cache) SetDefault(k string, x interface{}) { + c.Set(k, x, DefaultExpiration) +} + +// Add an item to the cache only if an item doesn't already exist for the given +// key, or if the existing item has expired. Returns an error otherwise. +func (c *cache) Add(k string, x interface{}, d time.Duration) error { + c.mu.Lock() + _, found := c.get(k) + if found { + c.mu.Unlock() + return fmt.Errorf("Item %s already exists", k) + } + c.set(k, x, d) + c.mu.Unlock() + return nil +} + +// Set a new value for the cache key only if it already exists, and the existing +// item hasn't expired. Returns an error otherwise. +func (c *cache) Replace(k string, x interface{}, d time.Duration) error { + c.mu.Lock() + _, found := c.get(k) + if !found { + c.mu.Unlock() + return fmt.Errorf("Item %s doesn't exist", k) + } + c.set(k, x, d) + c.mu.Unlock() + return nil +} + +// Get an item from the cache. Returns the item or nil, and a bool indicating +// whether the key was found. +func (c *cache) Get(k string) (interface{}, bool) { + c.mu.RLock() + // "Inlining" of get and Expired + item, found := c.items[k] + if !found { + c.mu.RUnlock() + return nil, false + } + if item.Expiration > 0 { + if time.Now().UnixNano() > item.Expiration { + c.mu.RUnlock() + return nil, false + } + } + c.mu.RUnlock() + return item.Object, true +} + +// GetWithExpiration returns an item and its expiration time from the cache. +// It returns the item or nil, the expiration time if one is set (if the item +// never expires a zero value for time.Time is returned), and a bool indicating +// whether the key was found. +func (c *cache) GetWithExpiration(k string) (interface{}, time.Time, bool) { + c.mu.RLock() + // "Inlining" of get and Expired + item, found := c.items[k] + if !found { + c.mu.RUnlock() + return nil, time.Time{}, false + } + + if item.Expiration > 0 { + if time.Now().UnixNano() > item.Expiration { + c.mu.RUnlock() + return nil, time.Time{}, false + } + + // Return the item and the expiration time + c.mu.RUnlock() + return item.Object, time.Unix(0, item.Expiration), true + } + + // If expiration <= 0 (i.e. no expiration time set) then return the item + // and a zeroed time.Time + c.mu.RUnlock() + return item.Object, time.Time{}, true +} + +func (c *cache) get(k string) (interface{}, bool) { + item, found := c.items[k] + if !found { + return nil, false + } + // "Inlining" of Expired + if item.Expiration > 0 { + if time.Now().UnixNano() > item.Expiration { + return nil, false + } + } + return item.Object, true +} + +// Increment an item of type int, int8, int16, int32, int64, uintptr, uint, +// uint8, uint32, or uint64, float32 or float64 by n. Returns an error if the +// item's value is not an integer, if it was not found, or if it is not +// possible to increment it by n. To retrieve the incremented value, use one +// of the specialized methods, e.g. IncrementInt64. +func (c *cache) Increment(k string, n int64) error { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return fmt.Errorf("Item %s not found", k) + } + switch v.Object.(type) { + case int: + v.Object = v.Object.(int) + int(n) + case int8: + v.Object = v.Object.(int8) + int8(n) + case int16: + v.Object = v.Object.(int16) + int16(n) + case int32: + v.Object = v.Object.(int32) + int32(n) + case int64: + v.Object = v.Object.(int64) + n + case uint: + v.Object = v.Object.(uint) + uint(n) + case uintptr: + v.Object = v.Object.(uintptr) + uintptr(n) + case uint8: + v.Object = v.Object.(uint8) + uint8(n) + case uint16: + v.Object = v.Object.(uint16) + uint16(n) + case uint32: + v.Object = v.Object.(uint32) + uint32(n) + case uint64: + v.Object = v.Object.(uint64) + uint64(n) + case float32: + v.Object = v.Object.(float32) + float32(n) + case float64: + v.Object = v.Object.(float64) + float64(n) + default: + c.mu.Unlock() + return fmt.Errorf("The value for %s is not an integer", k) + } + c.items[k] = v + c.mu.Unlock() + return nil +} + +// Increment an item of type float32 or float64 by n. Returns an error if the +// item's value is not floating point, if it was not found, or if it is not +// possible to increment it by n. Pass a negative number to decrement the +// value. To retrieve the incremented value, use one of the specialized methods, +// e.g. IncrementFloat64. +func (c *cache) IncrementFloat(k string, n float64) error { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return fmt.Errorf("Item %s not found", k) + } + switch v.Object.(type) { + case float32: + v.Object = v.Object.(float32) + float32(n) + case float64: + v.Object = v.Object.(float64) + n + default: + c.mu.Unlock() + return fmt.Errorf("The value for %s does not have type float32 or float64", k) + } + c.items[k] = v + c.mu.Unlock() + return nil +} + +// Increment an item of type int by n. Returns an error if the item's value is +// not an int, or if it was not found. If there is no error, the incremented +// value is returned. +func (c *cache) IncrementInt(k string, n int) (int, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(int) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an int", k) + } + nv := rv + n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Increment an item of type int8 by n. Returns an error if the item's value is +// not an int8, or if it was not found. If there is no error, the incremented +// value is returned. +func (c *cache) IncrementInt8(k string, n int8) (int8, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(int8) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an int8", k) + } + nv := rv + n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Increment an item of type int16 by n. Returns an error if the item's value is +// not an int16, or if it was not found. If there is no error, the incremented +// value is returned. +func (c *cache) IncrementInt16(k string, n int16) (int16, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(int16) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an int16", k) + } + nv := rv + n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Increment an item of type int32 by n. Returns an error if the item's value is +// not an int32, or if it was not found. If there is no error, the incremented +// value is returned. +func (c *cache) IncrementInt32(k string, n int32) (int32, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(int32) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an int32", k) + } + nv := rv + n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Increment an item of type int64 by n. Returns an error if the item's value is +// not an int64, or if it was not found. If there is no error, the incremented +// value is returned. +func (c *cache) IncrementInt64(k string, n int64) (int64, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(int64) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an int64", k) + } + nv := rv + n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Increment an item of type uint by n. Returns an error if the item's value is +// not an uint, or if it was not found. If there is no error, the incremented +// value is returned. +func (c *cache) IncrementUint(k string, n uint) (uint, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(uint) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an uint", k) + } + nv := rv + n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Increment an item of type uintptr by n. Returns an error if the item's value +// is not an uintptr, or if it was not found. If there is no error, the +// incremented value is returned. +func (c *cache) IncrementUintptr(k string, n uintptr) (uintptr, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(uintptr) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an uintptr", k) + } + nv := rv + n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Increment an item of type uint8 by n. Returns an error if the item's value +// is not an uint8, or if it was not found. If there is no error, the +// incremented value is returned. +func (c *cache) IncrementUint8(k string, n uint8) (uint8, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(uint8) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an uint8", k) + } + nv := rv + n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Increment an item of type uint16 by n. Returns an error if the item's value +// is not an uint16, or if it was not found. If there is no error, the +// incremented value is returned. +func (c *cache) IncrementUint16(k string, n uint16) (uint16, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(uint16) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an uint16", k) + } + nv := rv + n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Increment an item of type uint32 by n. Returns an error if the item's value +// is not an uint32, or if it was not found. If there is no error, the +// incremented value is returned. +func (c *cache) IncrementUint32(k string, n uint32) (uint32, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(uint32) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an uint32", k) + } + nv := rv + n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Increment an item of type uint64 by n. Returns an error if the item's value +// is not an uint64, or if it was not found. If there is no error, the +// incremented value is returned. +func (c *cache) IncrementUint64(k string, n uint64) (uint64, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(uint64) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an uint64", k) + } + nv := rv + n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Increment an item of type float32 by n. Returns an error if the item's value +// is not an float32, or if it was not found. If there is no error, the +// incremented value is returned. +func (c *cache) IncrementFloat32(k string, n float32) (float32, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(float32) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an float32", k) + } + nv := rv + n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Increment an item of type float64 by n. Returns an error if the item's value +// is not an float64, or if it was not found. If there is no error, the +// incremented value is returned. +func (c *cache) IncrementFloat64(k string, n float64) (float64, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(float64) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an float64", k) + } + nv := rv + n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Decrement an item of type int, int8, int16, int32, int64, uintptr, uint, +// uint8, uint32, or uint64, float32 or float64 by n. Returns an error if the +// item's value is not an integer, if it was not found, or if it is not +// possible to decrement it by n. To retrieve the decremented value, use one +// of the specialized methods, e.g. DecrementInt64. +func (c *cache) Decrement(k string, n int64) error { + // TODO: Implement Increment and Decrement more cleanly. + // (Cannot do Increment(k, n*-1) for uints.) + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return fmt.Errorf("Item not found") + } + switch v.Object.(type) { + case int: + v.Object = v.Object.(int) - int(n) + case int8: + v.Object = v.Object.(int8) - int8(n) + case int16: + v.Object = v.Object.(int16) - int16(n) + case int32: + v.Object = v.Object.(int32) - int32(n) + case int64: + v.Object = v.Object.(int64) - n + case uint: + v.Object = v.Object.(uint) - uint(n) + case uintptr: + v.Object = v.Object.(uintptr) - uintptr(n) + case uint8: + v.Object = v.Object.(uint8) - uint8(n) + case uint16: + v.Object = v.Object.(uint16) - uint16(n) + case uint32: + v.Object = v.Object.(uint32) - uint32(n) + case uint64: + v.Object = v.Object.(uint64) - uint64(n) + case float32: + v.Object = v.Object.(float32) - float32(n) + case float64: + v.Object = v.Object.(float64) - float64(n) + default: + c.mu.Unlock() + return fmt.Errorf("The value for %s is not an integer", k) + } + c.items[k] = v + c.mu.Unlock() + return nil +} + +// Decrement an item of type float32 or float64 by n. Returns an error if the +// item's value is not floating point, if it was not found, or if it is not +// possible to decrement it by n. Pass a negative number to decrement the +// value. To retrieve the decremented value, use one of the specialized methods, +// e.g. DecrementFloat64. +func (c *cache) DecrementFloat(k string, n float64) error { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return fmt.Errorf("Item %s not found", k) + } + switch v.Object.(type) { + case float32: + v.Object = v.Object.(float32) - float32(n) + case float64: + v.Object = v.Object.(float64) - n + default: + c.mu.Unlock() + return fmt.Errorf("The value for %s does not have type float32 or float64", k) + } + c.items[k] = v + c.mu.Unlock() + return nil +} + +// Decrement an item of type int by n. Returns an error if the item's value is +// not an int, or if it was not found. If there is no error, the decremented +// value is returned. +func (c *cache) DecrementInt(k string, n int) (int, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(int) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an int", k) + } + nv := rv - n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Decrement an item of type int8 by n. Returns an error if the item's value is +// not an int8, or if it was not found. If there is no error, the decremented +// value is returned. +func (c *cache) DecrementInt8(k string, n int8) (int8, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(int8) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an int8", k) + } + nv := rv - n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Decrement an item of type int16 by n. Returns an error if the item's value is +// not an int16, or if it was not found. If there is no error, the decremented +// value is returned. +func (c *cache) DecrementInt16(k string, n int16) (int16, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(int16) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an int16", k) + } + nv := rv - n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Decrement an item of type int32 by n. Returns an error if the item's value is +// not an int32, or if it was not found. If there is no error, the decremented +// value is returned. +func (c *cache) DecrementInt32(k string, n int32) (int32, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(int32) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an int32", k) + } + nv := rv - n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Decrement an item of type int64 by n. Returns an error if the item's value is +// not an int64, or if it was not found. If there is no error, the decremented +// value is returned. +func (c *cache) DecrementInt64(k string, n int64) (int64, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(int64) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an int64", k) + } + nv := rv - n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Decrement an item of type uint by n. Returns an error if the item's value is +// not an uint, or if it was not found. If there is no error, the decremented +// value is returned. +func (c *cache) DecrementUint(k string, n uint) (uint, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(uint) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an uint", k) + } + nv := rv - n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Decrement an item of type uintptr by n. Returns an error if the item's value +// is not an uintptr, or if it was not found. If there is no error, the +// decremented value is returned. +func (c *cache) DecrementUintptr(k string, n uintptr) (uintptr, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(uintptr) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an uintptr", k) + } + nv := rv - n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Decrement an item of type uint8 by n. Returns an error if the item's value is +// not an uint8, or if it was not found. If there is no error, the decremented +// value is returned. +func (c *cache) DecrementUint8(k string, n uint8) (uint8, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(uint8) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an uint8", k) + } + nv := rv - n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Decrement an item of type uint16 by n. Returns an error if the item's value +// is not an uint16, or if it was not found. If there is no error, the +// decremented value is returned. +func (c *cache) DecrementUint16(k string, n uint16) (uint16, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(uint16) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an uint16", k) + } + nv := rv - n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Decrement an item of type uint32 by n. Returns an error if the item's value +// is not an uint32, or if it was not found. If there is no error, the +// decremented value is returned. +func (c *cache) DecrementUint32(k string, n uint32) (uint32, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(uint32) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an uint32", k) + } + nv := rv - n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Decrement an item of type uint64 by n. Returns an error if the item's value +// is not an uint64, or if it was not found. If there is no error, the +// decremented value is returned. +func (c *cache) DecrementUint64(k string, n uint64) (uint64, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(uint64) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an uint64", k) + } + nv := rv - n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Decrement an item of type float32 by n. Returns an error if the item's value +// is not an float32, or if it was not found. If there is no error, the +// decremented value is returned. +func (c *cache) DecrementFloat32(k string, n float32) (float32, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(float32) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an float32", k) + } + nv := rv - n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Decrement an item of type float64 by n. Returns an error if the item's value +// is not an float64, or if it was not found. If there is no error, the +// decremented value is returned. +func (c *cache) DecrementFloat64(k string, n float64) (float64, error) { + c.mu.Lock() + v, found := c.items[k] + if !found || v.Expired() { + c.mu.Unlock() + return 0, fmt.Errorf("Item %s not found", k) + } + rv, ok := v.Object.(float64) + if !ok { + c.mu.Unlock() + return 0, fmt.Errorf("The value for %s is not an float64", k) + } + nv := rv - n + v.Object = nv + c.items[k] = v + c.mu.Unlock() + return nv, nil +} + +// Delete an item from the cache. Does nothing if the key is not in the cache. +func (c *cache) Delete(k string) { + c.mu.Lock() + v, evicted := c.delete(k) + c.mu.Unlock() + if evicted { + c.onEvicted(k, v) + } +} + +func (c *cache) delete(k string) (interface{}, bool) { + if c.onEvicted != nil { + if v, found := c.items[k]; found { + delete(c.items, k) + return v.Object, true + } + } + delete(c.items, k) + return nil, false +} + +type keyAndValue struct { + key string + value interface{} +} + +// Delete all expired items from the cache. +func (c *cache) DeleteExpired() { + var evictedItems []keyAndValue + now := time.Now().UnixNano() + c.mu.Lock() + for k, v := range c.items { + // "Inlining" of expired + if v.Expiration > 0 && now > v.Expiration { + ov, evicted := c.delete(k) + if evicted { + evictedItems = append(evictedItems, keyAndValue{k, ov}) + } + } + } + c.mu.Unlock() + for _, v := range evictedItems { + c.onEvicted(v.key, v.value) + } +} + +// Sets an (optional) function that is called with the key and value when an +// item is evicted from the cache. (Including when it is deleted manually, but +// not when it is overwritten.) Set to nil to disable. +func (c *cache) OnEvicted(f func(string, interface{})) { + c.mu.Lock() + c.onEvicted = f + c.mu.Unlock() +} + +// Write the cache's items (using Gob) to an io.Writer. +// +// NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the +// documentation for NewFrom().) +func (c *cache) Save(w io.Writer) (err error) { + enc := gob.NewEncoder(w) + defer func() { + if x := recover(); x != nil { + err = fmt.Errorf("Error registering item types with Gob library") + } + }() + c.mu.RLock() + defer c.mu.RUnlock() + for _, v := range c.items { + gob.Register(v.Object) + } + err = enc.Encode(&c.items) + return +} + +// Save the cache's items to the given filename, creating the file if it +// doesn't exist, and overwriting it if it does. +// +// NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the +// documentation for NewFrom().) +func (c *cache) SaveFile(fname string) error { + fp, err := os.Create(fname) + if err != nil { + return err + } + err = c.Save(fp) + if err != nil { + fp.Close() + return err + } + return fp.Close() +} + +// Add (Gob-serialized) cache items from an io.Reader, excluding any items with +// keys that already exist (and haven't expired) in the current cache. +// +// NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the +// documentation for NewFrom().) +func (c *cache) Load(r io.Reader) error { + dec := gob.NewDecoder(r) + items := map[string]Item{} + err := dec.Decode(&items) + if err == nil { + c.mu.Lock() + defer c.mu.Unlock() + for k, v := range items { + ov, found := c.items[k] + if !found || ov.Expired() { + c.items[k] = v + } + } + } + return err +} + +// Load and add cache items from the given filename, excluding any items with +// keys that already exist in the current cache. +// +// NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the +// documentation for NewFrom().) +func (c *cache) LoadFile(fname string) error { + fp, err := os.Open(fname) + if err != nil { + return err + } + err = c.Load(fp) + if err != nil { + fp.Close() + return err + } + return fp.Close() +} + +// Copies all unexpired items in the cache into a new map and returns it. +func (c *cache) Items() map[string]Item { + c.mu.RLock() + defer c.mu.RUnlock() + m := make(map[string]Item, len(c.items)) + now := time.Now().UnixNano() + for k, v := range c.items { + // "Inlining" of Expired + if v.Expiration > 0 { + if now > v.Expiration { + continue + } + } + m[k] = v + } + return m +} + +// Returns the number of items in the cache. This may include items that have +// expired, but have not yet been cleaned up. +func (c *cache) ItemCount() int { + c.mu.RLock() + n := len(c.items) + c.mu.RUnlock() + return n +} + +// Delete all items from the cache. +func (c *cache) Flush() { + c.mu.Lock() + c.items = map[string]Item{} + c.mu.Unlock() +} + +type janitor struct { + Interval time.Duration + stop chan bool +} + +func (j *janitor) Run(c *cache) { + ticker := time.NewTicker(j.Interval) + for { + select { + case <-ticker.C: + c.DeleteExpired() + case <-j.stop: + ticker.Stop() + return + } + } +} + +func stopJanitor(c *Cache) { + c.janitor.stop <- true +} + +func runJanitor(c *cache, ci time.Duration) { + j := &janitor{ + Interval: ci, + stop: make(chan bool), + } + c.janitor = j + go j.Run(c) +} + +func newCache(de time.Duration, m map[string]Item) *cache { + if de == 0 { + de = -1 + } + c := &cache{ + defaultExpiration: de, + items: m, + } + return c +} + +func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache { + c := newCache(de, m) + // This trick ensures that the janitor goroutine (which--granted it + // was enabled--is running DeleteExpired on c forever) does not keep + // the returned C object from being garbage collected. When it is + // garbage collected, the finalizer stops the janitor goroutine, after + // which c can be collected. + C := &Cache{c} + if ci > 0 { + runJanitor(c, ci) + runtime.SetFinalizer(C, stopJanitor) + } + return C +} + +// Return a new cache with a given default expiration duration and cleanup +// interval. If the expiration duration is less than one (or NoExpiration), +// the items in the cache never expire (by default), and must be deleted +// manually. If the cleanup interval is less than one, expired items are not +// deleted from the cache before calling c.DeleteExpired(). +func New(defaultExpiration, cleanupInterval time.Duration) *Cache { + items := make(map[string]Item) + return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) +} + +// Return a new cache with a given default expiration duration and cleanup +// interval. If the expiration duration is less than one (or NoExpiration), +// the items in the cache never expire (by default), and must be deleted +// manually. If the cleanup interval is less than one, expired items are not +// deleted from the cache before calling c.DeleteExpired(). +// +// NewFrom() also accepts an items map which will serve as the underlying map +// for the cache. This is useful for starting from a deserialized cache +// (serialized using e.g. gob.Encode() on c.Items()), or passing in e.g. +// make(map[string]Item, 500) to improve startup performance when the cache +// is expected to reach a certain minimum size. +// +// Only the cache's methods synchronize access to this map, so it is not +// recommended to keep any references to the map around after creating a cache. +// If need be, the map can be accessed at a later point using c.Items() (subject +// to the same caveat.) +// +// Note regarding serialization: When using e.g. gob, make sure to +// gob.Register() the individual types stored in the cache before encoding a +// map retrieved with c.Items(), and to register those same types before +// decoding a blob containing an items map. +func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cache { + return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) +} diff --git a/vendor/github.com/patrickmn/go-cache/sharded.go b/vendor/github.com/patrickmn/go-cache/sharded.go new file mode 100644 index 000000000..bcc0538bc --- /dev/null +++ b/vendor/github.com/patrickmn/go-cache/sharded.go @@ -0,0 +1,192 @@ +package cache + +import ( + "crypto/rand" + "math" + "math/big" + insecurerand "math/rand" + "os" + "runtime" + "time" +) + +// This is an experimental and unexported (for now) attempt at making a cache +// with better algorithmic complexity than the standard one, namely by +// preventing write locks of the entire cache when an item is added. As of the +// time of writing, the overhead of selecting buckets results in cache +// operations being about twice as slow as for the standard cache with small +// total cache sizes, and faster for larger ones. +// +// See cache_test.go for a few benchmarks. + +type unexportedShardedCache struct { + *shardedCache +} + +type shardedCache struct { + seed uint32 + m uint32 + cs []*cache + janitor *shardedJanitor +} + +// djb2 with better shuffling. 5x faster than FNV with the hash.Hash overhead. +func djb33(seed uint32, k string) uint32 { + var ( + l = uint32(len(k)) + d = 5381 + seed + l + i = uint32(0) + ) + // Why is all this 5x faster than a for loop? + if l >= 4 { + for i < l-4 { + d = (d * 33) ^ uint32(k[i]) + d = (d * 33) ^ uint32(k[i+1]) + d = (d * 33) ^ uint32(k[i+2]) + d = (d * 33) ^ uint32(k[i+3]) + i += 4 + } + } + switch l - i { + case 1: + case 2: + d = (d * 33) ^ uint32(k[i]) + case 3: + d = (d * 33) ^ uint32(k[i]) + d = (d * 33) ^ uint32(k[i+1]) + case 4: + d = (d * 33) ^ uint32(k[i]) + d = (d * 33) ^ uint32(k[i+1]) + d = (d * 33) ^ uint32(k[i+2]) + } + return d ^ (d >> 16) +} + +func (sc *shardedCache) bucket(k string) *cache { + return sc.cs[djb33(sc.seed, k)%sc.m] +} + +func (sc *shardedCache) Set(k string, x interface{}, d time.Duration) { + sc.bucket(k).Set(k, x, d) +} + +func (sc *shardedCache) Add(k string, x interface{}, d time.Duration) error { + return sc.bucket(k).Add(k, x, d) +} + +func (sc *shardedCache) Replace(k string, x interface{}, d time.Duration) error { + return sc.bucket(k).Replace(k, x, d) +} + +func (sc *shardedCache) Get(k string) (interface{}, bool) { + return sc.bucket(k).Get(k) +} + +func (sc *shardedCache) Increment(k string, n int64) error { + return sc.bucket(k).Increment(k, n) +} + +func (sc *shardedCache) IncrementFloat(k string, n float64) error { + return sc.bucket(k).IncrementFloat(k, n) +} + +func (sc *shardedCache) Decrement(k string, n int64) error { + return sc.bucket(k).Decrement(k, n) +} + +func (sc *shardedCache) Delete(k string) { + sc.bucket(k).Delete(k) +} + +func (sc *shardedCache) DeleteExpired() { + for _, v := range sc.cs { + v.DeleteExpired() + } +} + +// Returns the items in the cache. This may include items that have expired, +// but have not yet been cleaned up. If this is significant, the Expiration +// fields of the items should be checked. Note that explicit synchronization +// is needed to use a cache and its corresponding Items() return values at +// the same time, as the maps are shared. +func (sc *shardedCache) Items() []map[string]Item { + res := make([]map[string]Item, len(sc.cs)) + for i, v := range sc.cs { + res[i] = v.Items() + } + return res +} + +func (sc *shardedCache) Flush() { + for _, v := range sc.cs { + v.Flush() + } +} + +type shardedJanitor struct { + Interval time.Duration + stop chan bool +} + +func (j *shardedJanitor) Run(sc *shardedCache) { + j.stop = make(chan bool) + tick := time.Tick(j.Interval) + for { + select { + case <-tick: + sc.DeleteExpired() + case <-j.stop: + return + } + } +} + +func stopShardedJanitor(sc *unexportedShardedCache) { + sc.janitor.stop <- true +} + +func runShardedJanitor(sc *shardedCache, ci time.Duration) { + j := &shardedJanitor{ + Interval: ci, + } + sc.janitor = j + go j.Run(sc) +} + +func newShardedCache(n int, de time.Duration) *shardedCache { + max := big.NewInt(0).SetUint64(uint64(math.MaxUint32)) + rnd, err := rand.Int(rand.Reader, max) + var seed uint32 + if err != nil { + os.Stderr.Write([]byte("WARNING: go-cache's newShardedCache failed to read from the system CSPRNG (/dev/urandom or equivalent.) Your system's security may be compromised. Continuing with an insecure seed.\n")) + seed = insecurerand.Uint32() + } else { + seed = uint32(rnd.Uint64()) + } + sc := &shardedCache{ + seed: seed, + m: uint32(n), + cs: make([]*cache, n), + } + for i := 0; i < n; i++ { + c := &cache{ + defaultExpiration: de, + items: map[string]Item{}, + } + sc.cs[i] = c + } + return sc +} + +func unexportedNewSharded(defaultExpiration, cleanupInterval time.Duration, shards int) *unexportedShardedCache { + if defaultExpiration == 0 { + defaultExpiration = -1 + } + sc := newShardedCache(shards, defaultExpiration) + SC := &unexportedShardedCache{sc} + if cleanupInterval > 0 { + runShardedJanitor(sc, cleanupInterval) + runtime.SetFinalizer(SC, stopShardedJanitor) + } + return SC +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e2cc4e1c9..6a652ab09 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -227,6 +227,9 @@ github.com/nicksnyder/go-i18n/v2/internal/plural # github.com/oklog/ulid v1.3.1 ## explicit github.com/oklog/ulid +# github.com/patrickmn/go-cache v2.1.0+incompatible +## explicit +github.com/patrickmn/go-cache # github.com/pmezard/go-difflib v1.0.0 ## explicit github.com/pmezard/go-difflib/difflib