openplatform

package module
v0.0.0-...-df36dec Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

README

StuHelper Open Platform

第三方应用审批、逐项用户授权与撤回通知模块。模块拥有独立 PostgreSQL schema open_platform,业务数据通过公开接口读取。使用 AGPL-3.0-only。

当前实现

开发者提交名称、用途、HTTPS 回调与信息范围。管理员逐项审批并设置授权期限、令牌期限和 UTC 日调用配额。登录不会自动授予业务信息权限。

稳定范围为 student_status、schools、qq_binding、profile、schedule、catalog、public_reviews。目录与公开评价仍由提供方执行当前内容准入。课表读取本人有效选课记录对应的排课,空 meetings 表示没有提供排课,不能推断用户空闲。QQ 必须来自可信绑定来源,缺失来源返回 503。

授权使用单次授权码与 PKCE S256,精确匹配登记回调。每个账号最多保留 100 个未过期、未决定的授权请求。用户会话创建 5 分钟授权请求,服务端绑定本人、应用修订、范围、回调和 state。决定返回回调 URL,客户端必须核验 state 与 iss。授权码有效 60 秒。

应用类型为 public 或 confidential。两类均要求 PKCE,confidential 还要求 HTTP Basic 应用凭据。访问令牌最长 1 小时,不发行刷新令牌;交换结果不明时重新发起授权。业务令牌不接受作为用户会话。

应用凭据仅首次轮换返回明文,持久化哈希。相同幂等键重试返回 retrievable=false;丢失响应后读取修订并重新轮换。授权码由私钥 MAC 派生,库中只存哈希,幂等结果不包含 code。CodeKey 必须为 32 随机字节,由私有配置管理。

每次业务读取前后检查当前应用、授权、期限和账号状态。撤回范围使已有令牌立即失效;保留范围需要新授权码换取新令牌。管理员变更使原授权失效,重新启用需用户重新授权。

注销在同一事务封存授权账号、停用名下应用。每个开发者最多 100 个应用。恢复由管理员另行处理;模块没有自动复活账号或旧令牌的入口。

Work 每批最多 100 条、最多 10 秒,为到期、停用与注销授权生成通知。即时拒绝读取不依赖后台进度。第三方通过应用凭据或开发者会话分页读取通知,顺序游标支持继续读取后续通知。

通知支持 received、deleted 和 retention_exception 回报,法定保留例外必须填写理由。seen_at 仅表示读取,deleted 仅记录第三方声明。已停用应用的凭据只可读取和反馈通知,不能恢复数据权限。

列表每页最多 100 条。提供方调用限时 3 秒,数据库操作限时 5 秒。应用日配额在真实数据库原子扣减,授权后的调用尝试包括提供方失败均计入。模块不缓存第三方读取的业务数据。

业务记录、必要审计与幂等结果同事务提交。授权和通知保留以供账号管理和审计,目前没有自动删除历史记录的任务。数据库备份与保留由私有运维策略管理,不增加付费存储或上传检查产物。

接口与检查

接口定义见 OpenAPI,生成客户端位于 sdk。运行 make generate 更新客户端,make check 检查生成一致性、仓库边界、文档、竞态与真实 PostgreSQL。

检查需要 STUHELPER_OPEN_PLATFORM_TEST_DSN 指向隔离测试服务器。CI 使用最新稳定 PostgreSQL。业务运行身份仅获得本 schema 的表权限,requests、events 和 tokens 只有 SELECT、INSERT,序列只允许 USAGE。迁移身份单独使用。

验收场景

场景 检查
OP-01:申请和审批 非管理员拒绝、未审批拒绝、批准范围与用户范围交集、回调和 PKCE 绑定、事务回滚
OP-03:授权失效 部分撤回、在途读取撤回、单次交换、过期、配额、账号注销与可恢复通知
OP-04:登录不自动授权 用户会话不能替代业务令牌,伪造 owner 和未知字段被拒绝

真实账号、第三方应用联调和生产迁移未验收。后端组合负责连接事实提供方并启动有界后台处理。本仓不包含客户端页面或生产配置。

Documentation

Overview

Package openplatform owns approved applications and separately consented business grants.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInput           = errors.New("invalid_input")
	ErrUnauthenticated = errors.New("unauthenticated")
	ErrForbidden       = errors.New("forbidden")
	ErrNotFound        = errors.New("not_found")
	ErrConflict        = errors.New("conflict")
	ErrUnavailable     = errors.New("unavailable")
	ErrQuota           = errors.New("quota_exceeded")
	ErrClient          = errors.New("invalid_client")
)
View Source
var Scopes = []string{"student_status", "schools", "qq_binding", "profile", "schedule", "catalog", "public_reviews"}

Functions

func OpenAPI

func OpenAPI() []byte

Types

type Acknowledgement

type Acknowledgement struct {
	Status string `json:"status"`
	Note   string `json:"note"`
}

type AppRecord

type AppRecord struct {
	View              Application
	Owner, SecretHash string
}

type Application

type Application struct {
	ID                 string    `json:"id"`
	Name               string    `json:"name"`
	Purpose            string    `json:"purpose"`
	RedirectURIs       []string  `json:"redirect_uris"`
	RequestedScopes    []string  `json:"requested_scopes"`
	ApprovedScopes     []string  `json:"approved_scopes"`
	ClientType         string    `json:"client_type"`
	State              string    `json:"state"`
	Note               string    `json:"note"`
	Revision           int64     `json:"revision"`
	CredentialRevision int64     `json:"credential_revision"`
	Policy             Policy    `json:"policy"`
	CreatedAt          time.Time `json:"created_at"`
}

type ApplicationInput

type ApplicationInput struct {
	Name            string   `json:"name"`
	Purpose         string   `json:"purpose"`
	RedirectURIs    []string `json:"redirect_uris"`
	RequestedScopes []string `json:"requested_scopes"`
	ClientType      string   `json:"client_type"`
	Revision        int64    `json:"revision,omitempty"`
}

type AuthorizationResult

type AuthorizationResult struct {
	RedirectURI string `json:"redirect_uri"`
}

type CatalogItem

type CatalogItem struct {
	ID           string `json:"id"`
	Kind         string `json:"kind"`
	SchoolID     string `json:"school_id"`
	Name         string `json:"name"`
	CourseID     string `json:"course_id,omitempty"`
	TermID       string `json:"term_id,omitempty"`
	DepartmentID string `json:"department_id,omitempty"`
	Biography    string `json:"biography,omitempty"`
	Revision     int64  `json:"revision"`
}
type Consent struct {
	ID          string      `json:"id"`
	Application Application `json:"application"`
	Scopes      []string    `json:"requested_scopes"`
	RedirectURI string      `json:"redirect_uri"`
	ExpiresAt   time.Time   `json:"expires_at"`
}

type ConsentDecision

type ConsentDecision struct {
	Decision string   `json:"decision"`
	Scopes   []string `json:"scopes"`
}

type ConsentInput

type ConsentInput struct {
	ClientID    string   `json:"client_id"`
	RedirectURI string   `json:"redirect_uri"`
	State       string   `json:"state"`
	Scopes      []string `json:"scope"`
	Challenge   string   `json:"code_challenge"`
	Method      string   `json:"code_challenge_method"`
}

type ConsentRecord

type ConsentRecord struct {
	View          Consent
	Owner         string
	Input         ConsentInput
	Decision      string
	Selected      []string
	CodeHash      string
	CodeExpiresAt time.Time
	Consumed      bool
	GrantID       string
	GrantRevision int64
}

type Credential

type Credential struct {
	AppID       string `json:"app_id"`
	Revision    int64  `json:"revision"`
	Secret      string `json:"client_secret,omitempty"`
	Retrievable bool   `json:"retrievable"`
}

type Data

type Data struct {
	Subject       string          `json:"subject"`
	Scope         string          `json:"scope"`
	StudentStatus *StudentStatus  `json:"student_status,omitempty"`
	Schools       *[]School       `json:"schools,omitempty"`
	QQBinding     *QQBinding      `json:"qq_binding,omitempty"`
	Profile       *Profile        `json:"profile,omitempty"`
	Schedule      *[]ScheduleItem `json:"schedule,omitempty"`
	Catalog       *[]CatalogItem  `json:"catalog,omitempty"`
	Reviews       *[]ReviewItem   `json:"public_reviews,omitempty"`
	Next          string          `json:"next,omitempty"`
}

Data uses closed projections so a provider cannot accidentally return raw credentials or verification material.

type Grant

type Grant struct {
	ID        string    `json:"id"`
	AppID     string    `json:"app_id"`
	AppName   string    `json:"app_name"`
	Subject   string    `json:"subject"`
	Scopes    []string  `json:"scopes"`
	State     string    `json:"state"`
	ExpiresAt time.Time `json:"expires_at"`
	Revision  int64     `json:"revision"`
}

type GrantRecord

type GrantRecord struct {
	View        Grant
	Owner       string
	AppRevision int64
}

type Notice

type Notice struct {
	Sequence        int64      `json:"sequence"`
	ID              string     `json:"id"`
	AppID           string     `json:"app_id"`
	Subject         string     `json:"subject"`
	Scopes          []string   `json:"scopes"`
	Reason          string     `json:"reason"`
	CreatedAt       time.Time  `json:"created_at"`
	SeenAt          *time.Time `json:"seen_at,omitempty"`
	Acknowledgement string     `json:"acknowledgement,omitempty"`
	Note            string     `json:"note,omitempty"`
	AcknowledgedAt  *time.Time `json:"acknowledged_at,omitempty"`
}

type Page

type Page[T any] struct {
	Items []T    `json:"items"`
	Next  string `json:"next,omitempty"`
}

type Policy

type Policy struct {
	GrantSeconds int `json:"grant_seconds"`
	TokenSeconds int `json:"token_seconds"`
	DailyQuota   int `json:"daily_quota"`
}

type Principal

type Principal struct {
	Owner string
	Admin bool
}

type Profile

type Profile struct {
	ID       string `json:"id"`
	Nickname string `json:"nickname"`
	Bio      string `json:"bio"`
}

type QQBinding

type QQBinding struct {
	Bound  bool   `json:"bound"`
	Number string `json:"number,omitempty"`
}

type Query

type Query struct {
	ID, Kind, Query, TermID, TargetID, After string
	Limit                                    int
}

type Repository

type Repository interface {
	Transaction(context.Context, string, string, func(Transaction) error) error
	LocateConsent(context.Context, string) (ConsentRecord, error)
	LocateCode(context.Context, string) (ConsentRecord, error)
	LocateToken(context.Context, string) (TokenRecord, GrantRecord, error)
	LocateGrant(context.Context, string) (GrantRecord, error)
	WorkCandidates(context.Context, time.Time, int) ([]GrantRecord, error)
}

type ReviewInput

type ReviewInput struct {
	Revision       int64    `json:"revision"`
	Decision       string   `json:"decision"`
	ApprovedScopes []string `json:"approved_scopes"`
	Policy         Policy   `json:"policy"`
	Note           string   `json:"note"`
}

type ReviewItem

type ReviewItem struct {
	ID        string    `json:"id"`
	Kind      string    `json:"kind"`
	TargetID  string    `json:"target_id"`
	Body      string    `json:"body"`
	Ratings   [5]int    `json:"ratings"`
	CreatedAt time.Time `json:"created_at"`
}

type RevokeInput

type RevokeInput struct {
	Revision int64    `json:"revision"`
	Scopes   []string `json:"scopes"`
}

type ScheduleItem

type ScheduleItem struct {
	ID         string            `json:"id"`
	CourseID   string            `json:"course_id"`
	CourseName string            `json:"course_name"`
	TermID     string            `json:"term_id"`
	ClassID    string            `json:"class_id"`
	Meetings   []ScheduleMeeting `json:"meetings"`
	FetchedAt  time.Time         `json:"fetched_at"`
	Active     bool              `json:"active"`
}

type ScheduleMeeting

type ScheduleMeeting struct {
	ID       string `json:"id"`
	TimeText string `json:"time_text"`
	Location string `json:"location"`
}

type School

type School struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

type Service

type Service struct {
	Repo    Repository
	Source  Source
	Now     func() time.Time
	Random  io.Reader
	CodeKey []byte
	Issuer  string
}

func (*Service) Acknowledge

func (s *Service) Acknowledge(ctx context.Context, p Principal, app, secret, id, key string, in Acknowledgement) (Notice, error)

func (*Service) Application

func (s *Service) Application(ctx context.Context, p Principal, id string) (Application, error)

func (*Service) Applications

func (s *Service) Applications(ctx context.Context, p Principal, admin bool, after string, limit int) (Page[Application], error)

func (*Service) Apply

func (s *Service) Apply(ctx context.Context, p Principal, id, key string, in ApplicationInput) (Application, error)

func (*Service) CloseAccount

func (s *Service) CloseAccount(ctx context.Context, owner, closureID string) error

CloseAccount installs a permanent grant-owner fence and application epochs in the same transaction. It is a trusted module port; there is no HTTP route that accepts another account key.

func (*Service) Consent

func (s *Service) Consent(ctx context.Context, p Principal, id string) (Consent, error)

func (*Service) Decide

func (s *Service) Decide(ctx context.Context, p Principal, id, key string, in ConsentDecision) (AuthorizationResult, error)

func (*Service) Exchange

func (s *Service) Exchange(ctx context.Context, in TokenInput) (Token, error)

func (*Service) Grants

func (s *Service) Grants(ctx context.Context, p Principal, after string, limit int) (Page[Grant], error)

func (*Service) Notices

func (s *Service) Notices(ctx context.Context, p Principal, app, secret, after string, limit int) (Page[Notice], error)

func (*Service) PrepareConsent

func (s *Service) PrepareConsent(ctx context.Context, p Principal, key string, in ConsentInput) (Consent, error)

func (*Service) Read

func (s *Service) Read(ctx context.Context, token, scope string, q Query) (Data, error)

func (*Service) Review

func (s *Service) Review(ctx context.Context, p Principal, id, key string, in ReviewInput) (Application, error)

func (*Service) Revoke

func (s *Service) Revoke(ctx context.Context, p Principal, id, key string, in RevokeInput) (Grant, error)

func (*Service) RotateCredential

func (s *Service) RotateCredential(ctx context.Context, p Principal, id, key string, revision int64) (Credential, error)

func (*Service) Validate

func (s *Service) Validate() error

Validate checks required dependencies and private protocol configuration.

func (*Service) Work

func (s *Service) Work(ctx context.Context, limit int) (int, error)

Work materializes at most limit pending termination notices. Reads already enforce the fence before this worker runs.

type Source

type Source interface {
	Read(context.Context, string, string, Query) (Data, error)
}

Source receives only the verified grant owner; content sources must enforce current admission.

type StudentStatus

type StudentStatus struct {
	Verified    bool      `json:"verified"`
	EvaluatedAt time.Time `json:"evaluated_at"`
}

type Token

type Token struct {
	AccessToken string `json:"access_token"`
	TokenType   string `json:"token_type"`
	ExpiresIn   int64  `json:"expires_in"`
	Scope       string `json:"scope"`
	Subject     string `json:"subject"`
}

type TokenInput

type TokenInput struct{ GrantType, ClientID, RedirectURI, Code, Verifier, ClientSecret string }

type TokenRecord

type TokenRecord struct {
	Hash, GrantID              string
	GrantRevision, AppRevision int64
	ExpiresAt                  time.Time
}

type Transaction

type Transaction interface {
	Closed(string) (bool, error)
	CloseOwner(string, string) error
	App(string) (AppRecord, error)
	Apps(string, bool, string, int) ([]AppRecord, error)
	SaveApp(AppRecord) error
	CountApps(string) (int, error)
	SuspendOwnedApps(string) error
	Consent(string) (ConsentRecord, error)
	PendingConsents(string, time.Time) (int, error)
	SaveConsent(ConsentRecord) error
	Grant(string) (GrantRecord, error)
	OwnerGrant(string, string) (GrantRecord, error)
	Grants(string, string, int) ([]GrantRecord, error)
	SaveGrant(GrantRecord) error
	Token(string) (TokenRecord, error)
	SaveToken(TokenRecord) error
	Quota(string, string, int) error
	Notice(string) (Notice, error)
	Notices(string, string, int) ([]Notice, error)
	SaveNotice(Notice) error
	Replay(string, string) (string, json.RawMessage, error)
	SaveReplay(string, string, string, json.RawMessage) error
	Event(string, string, string, string) error
}

Directories

Path Synopsis
Package sdk provides primitives to interact with the openapi HTTP API.
Package sdk provides primitives to interact with the openapi HTTP API.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL