代码拉取完成,页面将自动刷新
package cos
import (
"errors"
"fmt"
"strings"
)
// =============================================================================
// STS 临时凭证 policy 拼装与 STSScope 校验
//
// 本文件是 IssueSTS 的纯函数依赖层:把业务侧描述的 STSScope 转换为腾讯云 STS
// v2.0 policy 文档(policyDoc),同时完成最小权限原则下的入参强校验。
//
// 设计要点:
// - 不引入 qcloud-cos-sts-sdk 依赖,输出自定义中间结构 policyDoc,
// 由 sts.go 在调用 SDK 前一次性映射到 *sts.CredentialPolicy;
// - 全部为纯函数,无 goroutine、无 IO、无锁,可被多个 goroutine 并发调用;
// - 校验失败立即返回哨兵错误,禁止做「宽容兜底」。
// =============================================================================
// 哨兵错误:STSScope 校验与 IssueSTS 上下文校验。
//
// 集中在本文件维护,便于排障时按错误码一站式定位触发场景。
var (
// ErrSTSScopeActionEmpty 触发场景:STSScope.Action 为空切片或全部元素为空字符串。
ErrSTSScopeActionEmpty = errors.New("sts scope: action is empty")
// ErrSTSScopeActionTooMany 触发场景:去重后 STSScope.Action 长度 > stsActionMaxCount(20)。
ErrSTSScopeActionTooMany = errors.New("sts scope: action exceeds 20 entries")
// ErrSTSScopeActionInvalid 触发场景:单条 Action 不以 "name/cos:" 或 "name/sts:" 开头、
// 包含通配符 "*"、含空白字符(空格 / 制表符 / 换行符)或其它控制字符。
ErrSTSScopeActionInvalid = errors.New("sts scope: action invalid (must start with name/cos: or name/sts:, no wildcard, no whitespace)")
// ErrSTSScopeKeyMissing 触发场景:STSScope.Key 与 STSScope.KeyPrefix 同时为空。
ErrSTSScopeKeyMissing = errors.New("sts scope: key and key_prefix are both empty")
// ErrSTSScopeKeyInvalid 触发场景:Key / KeyPrefix 含 ".."、"*",或 KeyPrefix 等于
// 单一 "/"(等价于全桶授权,被禁用)。
ErrSTSScopeKeyInvalid = errors.New("sts scope: key/key_prefix invalid (forbid '..', '*', single '/')")
// ErrSTSAppIDMissing 触发场景:IssueSTS 阶段 resolvedInstance.STSAppID 为空,
// 且无法从 BucketURL 推断(桶名末段非纯数字,或未配置 BucketURL)。
ErrSTSAppIDMissing = errors.New("sts: app_id is empty (configure sts_app_id or use bucket_url containing -{appid})")
// ErrSTSRegionMissing 触发场景:IssueSTS 阶段 resolvedInstance.STSRegion 为空,
// 且无法从 BucketURL 推断(host 不符合 *.cos.{region}.myqcloud.com 模式)。
ErrSTSRegionMissing = errors.New("sts: region is empty (configure sts_region or use a bucket_url containing region)")
// ErrSTSBucketMissing 触发场景:IssueSTS 阶段 resolvedInstance.Bucket 为空,
// 且无法从 BucketURL 推断。多用于配置错误(BucketURL 缺失的 service-only 实例)。
ErrSTSBucketMissing = errors.New("sts: bucket is empty (configure bucket_url to enable sts)")
)
// 内置常量。
const (
// stsActionPrefixCOS / stsActionPrefixSTS 是 CAM Action 字面量唯一允许的前缀。
stsActionPrefixCOS = "name/cos:"
stsActionPrefixSTS = "name/sts:"
// stsMinDurationSecs / stsMaxDurationSecs 限定 Scope.DurationSecs 的合法区间。
// 越界时由 validateScope 截断到本区间,并通过 durationClamped 标志告知调用方需要打 Warn。
stsMinDurationSecs = 60
stsMaxDurationSecs = 7200
// stsActionMaxCount 单次签发允许携带的 Action 上限(去重后)。
stsActionMaxCount = 20
// stsSafetyPaddingSecs IssueSTS 返回前从 ExpiredTime 中扣除的安全垫秒数,
// 用于抵御本地时钟与腾讯云 STS 服务时钟之间的漂移。
stsSafetyPaddingSecs = 30
)
// policyDoc 与 policyStatement 是腾讯云 STS v2.0 policy 的本地中间表示。
//
// 字段命名严格对齐腾讯云协议:
//
// {
// "version": "2.0",
// "statement": [{ "effect": "allow", "action": [...], "resource": [...], "condition": {...} }]
// }
//
// 不导出本结构是为了避免成为 cos 包的对外契约:腾讯云协议演进时只需调整 policy.go
// 与 sts.go 之间的内部映射,不影响 IssueSTS 等公开 API。
type policyDoc struct {
Version string `json:"version"`
Statement []policyStatement `json:"statement"`
}
// policyStatement 与 policyDoc 一同构成 v2.0 policy 文档;condition 仅在非空时输出。
type policyStatement struct {
Effect string `json:"effect"`
Action []string `json:"action"`
Resource []string `json:"resource"`
Condition map[string]map[string]interface{} `json:"condition,omitempty"`
}
// validateScope 校验 STSScope 并返回规整后的副本。
//
// 行为:
// - Action:去重保序、强校验前缀 / 通配符 / 空白字符;
// - Key/KeyPrefix:互斥但至少一个非空,禁用 ".."、"*"、单 "/"、空字符串;
// - DurationSecs:0 时使用 fallbackDuration;非 0 时越界则截断到 [60, 7200],
// 并通过 durationClamped=true 告知调用方需要按你 ② 规约打 Warn 日志。
//
// 返回值:
// - normalized:可安全用于 buildPolicy 的规整副本(不修改入参);
// - dedupedCount:去重前后的条数差,> 0 时调用方可选打 Debug 日志;
// - durationClamped:DurationSecs 是否被截断(true 时调用方必须打 Warn)。
func validateScope(scope STSScope, fallbackDuration int) (normalized STSScope, dedupedCount int, durationClamped bool, err error) {
// 1. Action 校验 + 去重
if len(scope.Action) == 0 {
return STSScope{}, 0, false, ErrSTSScopeActionEmpty
}
seen := make(map[string]struct{}, len(scope.Action))
dedupActions := make([]string, 0, len(scope.Action))
for _, a := range scope.Action {
if !isValidAction(a) {
// 单独区分 "空字符串" 与 "格式错误":空字符串归类为 ActionEmpty,
// 让上游能区分「忘了填」与「填错了」两种诊断路径。
if a == "" {
return STSScope{}, 0, false, ErrSTSScopeActionEmpty
}
return STSScope{}, 0, false, fmt.Errorf("%w: %q", ErrSTSScopeActionInvalid, a)
}
if _, dup := seen[a]; dup {
continue
}
seen[a] = struct{}{}
dedupActions = append(dedupActions, a)
}
if len(dedupActions) == 0 {
return STSScope{}, 0, false, ErrSTSScopeActionEmpty
}
if len(dedupActions) > stsActionMaxCount {
return STSScope{}, 0, false, fmt.Errorf("%w: got %d", ErrSTSScopeActionTooMany, len(dedupActions))
}
// 2. Key / KeyPrefix 校验
hasKey := scope.Key != ""
hasPrefix := scope.KeyPrefix != ""
if !hasKey && !hasPrefix {
return STSScope{}, 0, false, ErrSTSScopeKeyMissing
}
if hasKey {
if !isValidKeyOrPrefix(scope.Key, false) {
return STSScope{}, 0, false, fmt.Errorf("%w: key=%q", ErrSTSScopeKeyInvalid, scope.Key)
}
}
if hasPrefix {
if !isValidKeyOrPrefix(scope.KeyPrefix, true) {
return STSScope{}, 0, false, fmt.Errorf("%w: key_prefix=%q", ErrSTSScopeKeyInvalid, scope.KeyPrefix)
}
}
// 3. DurationSecs 处理:0 → fallback;非 0 越界 → 截断
dur := scope.DurationSecs
switch {
case dur == 0:
dur = fallbackDuration
case dur < stsMinDurationSecs:
dur = stsMinDurationSecs
durationClamped = true
case dur > stsMaxDurationSecs:
dur = stsMaxDurationSecs
durationClamped = true
}
// fallback 自身越界时再做一次保护性截断(极少数错误配置场景)
if dur < stsMinDurationSecs {
dur = stsMinDurationSecs
} else if dur > stsMaxDurationSecs {
dur = stsMaxDurationSecs
}
dedupedCount = len(scope.Action) - len(dedupActions)
// 4. 复制副本,避免对入参做任何写操作(保持函数纯净)
normalized = STSScope{
Action: dedupActions,
Key: scope.Key,
KeyPrefix: scope.KeyPrefix,
DurationSecs: dur,
MaxObjectSize: scope.MaxObjectSize,
ContentType: scope.ContentType,
SourceIP: append([]string(nil), scope.SourceIP...),
}
return normalized, dedupedCount, durationClamped, nil
}
// isValidAction 判断单条 Action 字面量是否合法。
//
// 规则:
// - 必须以 "name/cos:" 或 "name/sts:" 开头;
// - 不允许包含 "*"(即使 "name/cos:Get*" 这种带前缀的通配也禁止);
// - 不允许包含空白字符(空格 / 制表符 / 换行 / 回车)与控制字符。
func isValidAction(a string) bool {
if a == "" {
return false
}
if !strings.HasPrefix(a, stsActionPrefixCOS) && !strings.HasPrefix(a, stsActionPrefixSTS) {
return false
}
if strings.ContainsRune(a, '*') {
return false
}
for i := 0; i < len(a); i++ {
// 拒绝所有 ASCII 控制字符(含 \t \n \r)以及裸空格
if a[i] <= 0x20 || a[i] == 0x7F {
return false
}
}
return true
}
// isValidKeyOrPrefix 判断 Key / KeyPrefix 是否合法。
//
// 共同规则:
// - 不允许空字符串、不允许包含 ".."(防止路径穿越)、不允许包含 "*"(避免提权)。
//
// 仅 KeyPrefix 额外规则:
// - 不允许等于单一 "/"(等价于全桶授权,违反最小权限原则)。
//
// 参数 isPrefix=true 时按 KeyPrefix 语义校验。
func isValidKeyOrPrefix(s string, isPrefix bool) bool {
if s == "" {
return false
}
if strings.Contains(s, "..") {
return false
}
if strings.Contains(s, "*") {
return false
}
if isPrefix && s == "/" {
return false
}
return true
}
// buildResources 根据 scope 与上下文(region/appid/bucket)拼接 COS 资源 ARN 列表。
//
// 资源模板:qcs::cos:{region}:uid/{appid}:{bucket}/{key|prefix*}
// - Key 模式:原样拼接(精确匹配单对象);
// - KeyPrefix 模式:自动追加 "*"(COS 资源 ARN 通配语法),实现前缀授权。
//
// region / appID / bucket 任一为空时返回对应哨兵错误:调用方据此可立即判定是
// 「配置缺失」还是「scope 自身错误」,方便分级排障。
func buildResources(scope STSScope, region, appID, bucket string) ([]string, error) {
if region == "" {
return nil, ErrSTSRegionMissing
}
if appID == "" {
return nil, ErrSTSAppIDMissing
}
if bucket == "" {
return nil, ErrSTSBucketMissing
}
prefix := fmt.Sprintf("qcs::cos:%s:uid/%s:%s", region, appID, bucket)
resources := make([]string, 0, 1)
switch {
case scope.Key != "":
// COS ARN 路径分隔符固定使用 "/",与对象 key 自身的前导斜杠归一处理:
// 若 key 以 "/" 开头则去掉,避免出现 "bucket//foo" 双斜杠
key := strings.TrimPrefix(scope.Key, "/")
resources = append(resources, prefix+"/"+key)
case scope.KeyPrefix != "":
p := strings.TrimPrefix(scope.KeyPrefix, "/")
// 末尾必须 "*":腾讯云 ARN 协议要求显式通配
if !strings.HasSuffix(p, "*") {
p += "*"
}
resources = append(resources, prefix+"/"+p)
}
return resources, nil
}
// buildPolicy 把规整后的 STSScope 转换成 v2.0 policy 文档。
//
// 调用方典型流程:
//
// normalized, _, _, err := validateScope(scope, defaultDur)
// if err != nil { ... }
// doc, err := buildPolicy(normalized, region, appID, bucket)
//
// 注意 condition 仅在对应字段非零时输出(buildConditions),避免向 STS 服务发送
// 空 condition 影响调试。
func buildPolicy(scope STSScope, region, appID, bucket string) (*policyDoc, error) {
resources, err := buildResources(scope, region, appID, bucket)
if err != nil {
return nil, err
}
stmt := policyStatement{
Effect: "allow",
Action: append([]string(nil), scope.Action...),
Resource: resources,
}
if cond := buildConditions(scope); len(cond) > 0 {
stmt.Condition = cond
}
return &policyDoc{
Version: "2.0",
Statement: []policyStatement{stmt},
}, nil
}
// buildConditions 仅在对应字段非零时输出 condition;全为零则返回 nil。
//
// 字段映射(与腾讯云 v2.0 协议字面量保持一致,禁止改写大小写):
// - MaxObjectSize → numeric_less_than_equal["cos:content-length"]
// - ContentType → string_equal_if_exist["cos:content-type"]
// - SourceIP → ip_equal["qcs:ip"](数组形式,对应 CIDR 列表)
func buildConditions(scope STSScope) map[string]map[string]interface{} {
cond := make(map[string]map[string]interface{}, 3)
if scope.MaxObjectSize > 0 {
cond["numeric_less_than_equal"] = map[string]interface{}{
"cos:content-length": scope.MaxObjectSize,
}
}
if scope.ContentType != "" {
cond["string_equal_if_exist"] = map[string]interface{}{
"cos:content-type": scope.ContentType,
}
}
if len(scope.SourceIP) > 0 {
cond["ip_equal"] = map[string]interface{}{
"qcs:ip": append([]string(nil), scope.SourceIP...),
}
}
if len(cond) == 0 {
return nil
}
return cond
}
// scopeLogKind 返回 "key" / "prefix",用于 IssueSTS 的结构化日志 scope_key_kind 字段。
//
// 仅当 Key / KeyPrefix 至少一个非空时调用本函数;两者都空属于校验阶段就会被拦截
// 的情形(ErrSTSScopeKeyMissing),不会到达本函数。
func scopeLogKind(scope STSScope) string {
if scope.Key != "" {
return "key"
}
return "prefix"
}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。