Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 99 additions & 7 deletions monkeyai/backend/api/agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,28 @@ paths:
description: 调用密钥无效
'403':
description: 请求来源不允许
/api/v1/groups:
get:
summary: 搜索可分享的真实分组
description: 按名称不区分大小写进行字面子串匹配,仅返回未删除分组;不包含虚拟根组及成员信息。分享给父组可覆盖后代组成员。
parameters:
- in: query
name: q
required: true
schema: {type: string, minLength: 1, maxLength: 200}
description: 去除首尾空白后长度为 1—200 字节。
- in: query
name: limit
schema: {type: integer, minimum: 1, maximum: 100, default: 20}
responses:
'200':
description: 匹配的分组。
content:
application/json:
schema: {$ref: '#/components/schemas/SearchGroupsResponse'}
'400': {$ref: '#/components/responses/BadRequest'}
'401': {$ref: '#/components/responses/Unauthorized'}
'500': {$ref: '#/components/responses/InternalError'}
/api/v1/users:
get:
summary: 按用户名或邮箱查找分享接收用户
Expand Down Expand Up @@ -1109,9 +1131,8 @@ paths:
$ref: '#/components/responses/ResourceError500'
/api/v1/resources/shares:
post:
summary: 批量分享自己的资源给指定用户
description: 支持 model、rule、skill、connector、expert;个人规则共享后接收者可选择使用,但不能强制应用。个人 Connector 的接收方使用自己的独立凭证,个人专家的依赖仍需接收方获得使用授权。分享为追加的只读使用授权,不覆盖其他接收用户;重复授权不会产生重复记录。每批资源和用户各
1—100 项,任一资源非本人所有或任一接收用户无效时整批回滚。禁止分享给自己,不允许接收方转分享。
summary: 批量分享自己的资源给指定用户或分组
description: 支持 model、rule、skill、connector、expert;个人规则共享后接收者可选择使用,但不能强制应用。个人 Connector 的接收方使用自己的独立凭证,个人专家的依赖仍需接收方获得使用授权。分享为追加的只读使用授权,不覆盖其他授权;重复授权不会产生重复记录。每批资源 1—100 项,用户与分组 ID 合计 1—100 项;任一资源非本人所有、任一接收用户无效或分组不存在时整批回滚。禁止直接分享给自己或虚拟根组,不允许接收方转分享。组授权覆盖其后代组成员并随成员关系变化即时生效。
requestBody:
required: true
content:
Expand All @@ -1130,8 +1151,8 @@ paths:
'500':
$ref: '#/components/responses/ResourceError500'
delete:
summary: 批量撤销指定用户的资源分享
description: 仅所有者可以撤销。只移除本批指定资源与用户间的授权,不存在的授权视为成功;允许撤销已停用或已删除用户的授权。任一资源无权操作则整批回滚。
summary: 批量撤销指定用户或分组的资源分享
description: 仅所有者可以撤销。只移除本批指定资源与用户或分组间的直接授权,不影响其他用户/分组的授权;不存在的授权视为成功,允许撤销已停用或已删除的接收对象。任一资源无权操作则整批回滚。
requestBody:
required: true
content:
Expand Down Expand Up @@ -3387,6 +3408,23 @@ components:
email:
type: string
format: email
GroupSummary:
type: object
required: [id, name, parent_id]
properties:
id: {type: string, format: uuid}
name: {type: string}
parent_id:
type: string
format: uuid
description: 顶层分组的父 ID 为虚拟根组 ID;虚拟根组自身不可分享。
SearchGroupsResponse:
type: object
required: [groups]
properties:
groups:
type: array
items: {$ref: '#/components/schemas/GroupSummary'}
SearchUsersResponse:
type: object
required:
Expand Down Expand Up @@ -3552,6 +3590,11 @@ components:
type: number
exclusiveMinimum: 0
description: 服务端管理,用户不可修改。
shared_groups:
type: array
readOnly: true
items: {$ref: '#/components/schemas/GroupSummary'}
description: 当前所有者直接分享的分组,未分享时为空数组。
authorization:
type: object
required:
Expand All @@ -3574,9 +3617,9 @@ components:
format: date-time
ShareResourcesInput:
type: object
description: user_ids 与 group_ids 至少填写一项,原始数组合计最多 100 项;重复项去重。虚拟根组不可作为分享对象。
required:
- resources
- user_ids
properties:
resources:
type: array
Expand All @@ -3600,10 +3643,14 @@ components:
id: *id006
user_ids:
type: array
minItems: 1
maxItems: 100
items: *id006
description: 接收用户 ID,重复项自动去重。
group_ids:
type: array
maxItems: 100
items: *id006
description: 接收真实分组 ID,重复项自动去重;不展开成员。
ErrorResponse:
type: object
required: [error]
Expand Down Expand Up @@ -4011,6 +4058,11 @@ components:
creator:
$ref: '#/components/schemas/UserSummary'
description: 他人创建的用户模型返回创建者;自己的模型和系统模型不返回。
shared_groups:
type: array
readOnly: true
items: {$ref: '#/components/schemas/GroupSummary'}
description: 仅资源所有者可见的直接分享分组,未分享时为空数组;不展开成员。
shared_users:
type: array
items:
Expand Down Expand Up @@ -4301,6 +4353,11 @@ components:
AgentRule:
type: object
properties:
shared_groups:
type: array
readOnly: true
items: {$ref: '#/components/schemas/GroupSummary'}
description: 仅资源所有者可见的直接分享分组,未分享时为空数组;不展开成员。
shared_users:
type: array
readOnly: true
Expand Down Expand Up @@ -4342,6 +4399,11 @@ components:
AgentSkill:
type: object
properties:
shared_groups:
type: array
readOnly: true
items: {$ref: '#/components/schemas/GroupSummary'}
description: 仅资源所有者可见的直接分享分组,未分享时为空数组;不展开成员。
shared_users:
type: array
readOnly: true
Expand Down Expand Up @@ -4393,6 +4455,11 @@ components:
tags:
type: array
items: {$ref: '#/components/schemas/ResourceTag'}
shared_groups:
type: array
readOnly: true
items: {$ref: '#/components/schemas/GroupSummary'}
description: 仅资源所有者可见的直接分享分组,未分享时为空数组;不展开成员。
shared_users:
type: array
readOnly: true
Expand Down Expand Up @@ -4478,6 +4545,11 @@ components:
tags:
type: array
items: {$ref: '#/components/schemas/ResourceTag'}
shared_groups:
type: array
readOnly: true
items: {$ref: '#/components/schemas/GroupSummary'}
description: 仅资源所有者可见的直接分享分组,未分享时为空数组;不展开成员。
shared_users:
type: array
readOnly: true
Expand Down Expand Up @@ -4653,6 +4725,11 @@ components:
RuleResource:
type: object
properties:
shared_groups:
type: array
readOnly: true
items: {$ref: '#/components/schemas/GroupSummary'}
description: 仅资源所有者可见的直接分享分组,未分享时为空数组;不展开成员。
shared_users:
type: array
readOnly: true
Expand Down Expand Up @@ -4717,6 +4794,11 @@ components:
SkillResource:
type: object
properties:
shared_groups:
type: array
readOnly: true
items: {$ref: '#/components/schemas/GroupSummary'}
description: 仅资源所有者可见的直接分享分组,未分享时为空数组;不展开成员。
shared_users:
type: array
readOnly: true
Expand Down Expand Up @@ -4799,6 +4881,11 @@ components:
tags:
type: array
items: {$ref: '#/components/schemas/ResourceTag'}
shared_groups:
type: array
readOnly: true
items: {$ref: '#/components/schemas/GroupSummary'}
description: 仅资源所有者可见的直接分享分组,未分享时为空数组;不展开成员。
shared_users:
type: array
readOnly: true
Expand Down Expand Up @@ -4900,6 +4987,11 @@ components:
tags:
type: array
items: {$ref: '#/components/schemas/ResourceTag'}
shared_groups:
type: array
readOnly: true
items: {$ref: '#/components/schemas/GroupSummary'}
description: 仅资源所有者可见的直接分享分组,未分享时为空数组;不展开成员。
shared_users:
type: array
readOnly: true
Expand Down
2 changes: 1 addition & 1 deletion monkeyai/backend/internal/agentconfig/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ WITH RECURSIVE user_groups (
)
SELECT
jsonb_build_object('kind', resource_type, 'id', resource_id, 'required',
bool_or(usage_requirement = 'required'))
bool_or(usage_requirement = 'required'), 'explicit', bool_or(NOT rag.all_users))
FROM
resource_access_grants rag
WHERE
Expand Down
17 changes: 6 additions & 11 deletions monkeyai/backend/internal/agentconfig/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,10 @@ func (r *Resources) load(ctx context.Context, q resource.Queryer, user, kind str
if err != nil {
return c, err
}
personalRules := []string{}
for _, grant := range g {
if grant.String("kind") == "rule" && c.rules[grant.String("id")].String("ownership_type") == "user" {
personalRules = append(personalRules, grant.String("id"))
}
}
sharedRules, err := resource.SharedUsers(ctx, q, "rule", personalRules)
if err != nil {
return c, err
}
for _, o := range g {
if o.String("kind") == "rule" {
rule := c.rules[o.String("id")]
if rule == nil || (rule.String("ownership_type") == "user" && !slices.ContainsFunc(sharedRules[o.String("id")], func(shared resource.Object) bool { return shared.String("id") == user })) {
if rule == nil || (rule.String("ownership_type") == "user" && !o.Bool("explicit")) {
continue
}
}
Expand Down Expand Up @@ -289,10 +279,15 @@ func (r *Resources) list(ctx context.Context, q resource.Queryer, user, kind str
if err != nil {
return nil, err
}
groups, err := resource.SharedGroups(ctx, q, resourceType, owned)
if err != nil {
return nil, err
}
for _, dto := range out {
dto["user"] = people[ownerIDs[dto.String("id")]]
if shared, ok := users[dto.String("id")]; ok {
dto["shared_users"] = shared
dto["shared_groups"] = groups[dto.String("id")]
}
}
resource.Stable(out)
Expand Down
2 changes: 1 addition & 1 deletion monkeyai/backend/internal/agentconfig/sqlc/query.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions monkeyai/backend/internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ func newApplicationHandler(ctx context.Context, logger *slog.Logger, pool *pgxpo
endpoints := endpoint.NewService(endpoint.NewPostgres(pool), endpointAuth{identities}, logger, cfg.PublicURL).WithMaxConnections(cfg.EndpointMaxConnections)
endpoints.RegisterAgent(agent)
identities.RegisterAgent(agent)
group.NewService(pool).RegisterAgent(agent)
keys.RegisterAgent(agent)
models.RegisterAgent(agent)
rules.RegisterAgent(agent)
Expand Down
26 changes: 26 additions & 0 deletions monkeyai/backend/internal/app/personal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,32 @@ func testPersonalResources(t *testing.T, pool *pgxpool.Pool, handler http.Handle
if call("GET", expertPath+"/manifest", "b", "", nil, 200).Bool("available") {
t.Fatal("撤销共享规则后专家仍可用")
}
groupID := resource.ID()
if _, err := pool.Exec(t.Context(), `INSERT INTO groups(id,name) VALUES($1,'个人资源分享测试组')`, groupID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(t.Context(), `INSERT INTO group_users(group_id,user_id,assigned_by_user_id) VALUES($1,$2,$3)`, groupID, users[1], users[0]); err != nil {
t.Fatal(err)
}
groupRule := resource.ShareInput{Resources: ruleShare.Resources, GroupIDs: []string{groupID}}
call("POST", "/resources/shares", "a", "", groupRule, 204)
if !contains("rules", "b", ruleID) || !call("GET", expertPath+"/manifest", "b", "", nil, 200).Bool("available") {
t.Fatal("分组授权的规则未使专家可用")
}
ownerRule := call("GET", "/rules/"+ruleID, "a", "", nil, 200)
if groups := ownerRule["shared_groups"].([]any); len(groups) != 1 || groups[0].(map[string]any)["id"] != groupID {
t.Fatalf("规则分组授权未回显: %v", ownerRule)
}
call("DELETE", "/resources/shares", "a", "", groupRule, 204)
if contains("rules", "b", ruleID) || call("GET", expertPath+"/manifest", "b", "", nil, 200).Bool("available") {
t.Fatal("分组规则撤销后权限未收回")
}
if _, err := pool.Exec(t.Context(), `DELETE FROM group_users WHERE group_id=$1`, groupID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(t.Context(), `DELETE FROM groups WHERE id=$1`, groupID); err != nil {
t.Fatal(err)
}
expert = call("GET", expertPath, "a", "", nil, 200)
expert = call("PUT", expertPath, "a", etag(expert), resource.Object{"name": expert["name"], "rule_ids": []string{}}, 200)

Expand Down
Loading
Loading