package httpapi import ( "context" "encoding/json" "net/http" "strings" "time" "knowledge-governance/internal/governance" ) // 生产发布授权:管理员把"目标 + 快照 + 参数摘要 + 有效期"钉死。 // 换快照、换目标或改参数都会得到不同摘要,旧授权自然失效;到期或被撤销也不能用。 type libraryAuthorizationRequest struct { Scope string `json:"scope"` SnapshotID string `json:"snapshot_id"` // Action 决定授权的动作:sync(默认)或 restore。两者摘要不同, // 同步授权不能当恢复授权用,反之亦然。 Action string `json:"action"` ExpiresInMinutes int `json:"expires_in_minutes"` } type libraryAuthorizationStore interface { CreateLibraryAuthorization(ctx context.Context, item governance.LibraryReleaseAuthorization) error LibraryAuthorizations(ctx context.Context, scope string, limit int) ([]governance.LibraryReleaseAuthorization, error) RevokeLibraryAuthorization(ctx context.Context, id string, now time.Time) error } func libraryAuthorizationStoreOf(config Config) (libraryAuthorizationStore, bool) { store, ok := config.Store.(libraryAuthorizationStore) return store, ok } // librarySyncParamsDigest 与 POST /library/syncs 使用的摘要必须完全一致, // 否则授权永远匹配不上,或者更糟——能匹配上却对应别的参数。 func librarySyncParamsDigest(scope, snapshotID string) (string, error) { return governance.LibraryRequestDigest(scope, governance.LibraryJobSync, map[string]any{"target_snapshot_id": snapshotID}) } func libraryAuthorizationCreate(w http.ResponseWriter, r *http.Request, config Config) { if r.Method != http.MethodPost { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"}) return } identity, ok := sessionFrom(r.Context()) if !ok || roleRank(identity.Role) < roleRank(roleAdmin) { writeJSON(w, http.StatusForbidden, map[string]any{"error": "生产发布授权仅管理员可签发"}) return } store, ok := libraryAuthorizationStoreOf(config) if !ok { writeJSON(w, http.StatusServiceUnavailable, map[string]any{"error": "授权存储不可用"}) return } var request libraryAuthorizationRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&request); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) return } if request.Scope != "internal" && request.Scope != "external" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "scope must be internal or external"}) return } if strings.TrimSpace(request.SnapshotID) == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "snapshot_id required"}) return } // 有效期必须显式给出且很短:不提供"永久授权"这种危险默认值。 if request.ExpiresInMinutes < 5 || request.ExpiresInMinutes > 24*60 { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "expires_in_minutes must be between 5 and 1440"}) return } action := strings.TrimSpace(request.Action) if action == "" { action = "sync" } var digest string var err error switch action { case "sync": digest, err = librarySyncParamsDigest(request.Scope, request.SnapshotID) case "restore": digest, err = libraryRestoreParamsDigest(request.Scope, request.SnapshotID) default: writeJSON(w, http.StatusBadRequest, map[string]string{"error": "action must be sync or restore"}) return } if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "cannot digest params"}) return } now := time.Now().UTC() item := governance.LibraryReleaseAuthorization{ ID: "libauth-" + action + "-" + request.Scope + "-" + now.Format("20060102T150405.000000000"), Scope: request.Scope, SnapshotID: request.SnapshotID, ParamsDigest: digest, AuthorizedBy: identity.Username, ExpiresAt: now.Add(time.Duration(request.ExpiresInMinutes) * time.Minute), CreatedAt: now, } if err := store.CreateLibraryAuthorization(r.Context(), item); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "cannot record authorization"}) return } writeJSON(w, http.StatusCreated, map[string]any{"authorization": item}) } func libraryAuthorizationList(w http.ResponseWriter, r *http.Request, config Config) { scope, err := scopeFromQuery(r) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } store, ok := libraryAuthorizationStoreOf(config) if !ok { writeJSON(w, http.StatusServiceUnavailable, map[string]any{"available": false, "reason": "授权存储不可用", "authorizations": []any{}}) return } items, err := store.LibraryAuthorizations(r.Context(), scope, 50) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "cannot read authorizations"}) return } writeJSON(w, http.StatusOK, map[string]any{"available": true, "authorizations": items}) } func libraryAuthorizationRevoke(w http.ResponseWriter, r *http.Request, config Config) { identity, ok := sessionFrom(r.Context()) if !ok || roleRank(identity.Role) < roleRank(roleAdmin) { writeJSON(w, http.StatusForbidden, map[string]any{"error": "撤销生产发布授权仅管理员可执行"}) return } store, ok := libraryAuthorizationStoreOf(config) if !ok { writeJSON(w, http.StatusServiceUnavailable, map[string]any{"error": "授权存储不可用"}) return } id := strings.TrimSpace(r.PathValue("id")) if id == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "authorization id required"}) return } if err := store.RevokeLibraryAuthorization(r.Context(), id, time.Now().UTC()); err != nil { writeJSON(w, http.StatusConflict, map[string]any{"error": "授权不存在或已撤销"}) return } writeJSON(w, http.StatusOK, map[string]any{"revoked": id}) }