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
4 changes: 4 additions & 0 deletions README.testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
```bash
./tests/cf-acl/run.sh
./tests/cf-fileop/run.sh
./tests/cf-permission-many/run.sh
./tests/cf-dir-share/run.sh
# 目录分页另需 valac / libjansson 开发包
./tests/cf-dir-page/run.sh

Expand All @@ -33,6 +35,8 @@ go test -count=1 -run 'Cf[A-Z]' .
| `tests/cf-acl/run.sh` | ACL 解析、继承、优先级、权限不放宽不变量 | 数据库查询、RPC 和进程集成 |
| `tests/cf-dir-page/run.sh` | 生产 C 扫描、RPC、ACL 过滤及相邻 Hub 端点的共享分页用例 | 存储/数据库与进程间 RPC 使用 fixture,不等于部署 E2E |
| `tests/cf-fileop/run.sh` | 路径/操作词汇、provider 分发、空 provider 透传、C 写入口字段与参数形状 | 运行时变量值是否传对、完整 seaf-server 编译 |
| `tests/cf-permission-many/run.sh` | 批量权限传输、标量/入口一致性、`check_permission_by_path` 在"无库权限 + 目录共享"下的回退与失败语义 | Hub 的 V2 裁决本身(用 stub 能力),不是部署 E2E |
| `tests/cf-dir-share/run.sh` | 目录共享资格的请求形状、路径规范化、r/rw 映射,以及拒绝/故障/缺字段的判别 | 真实 Hub HTTP、JWT 与进程间调用(channel 为 stub) |
| Go `Cf*` 测试 | C/Go 词汇、JSON 字段、错误码、共享用例 | MySQL 依赖的上游测试 |
| `tests/cf-s3/run.sh` | C commit/fs/block S3 backend 与错误语义 | 未设置端点时会跳过,不构成 S3 验收 |
| `ci/run.py` | 上游构建、MySQL、C/Go fileserver 和 pytest | 需要 Linux 与完整依赖链 |
Expand Down
52 changes: 51 additions & 1 deletion common/cf-ext.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "cf-acl.h"
#include "cf-fileop-test.h"
#include "cf-lock.h"
#include "cf-v2-share.h"

typedef struct CfProvider {
char *name;
Expand All @@ -18,7 +19,15 @@ typedef struct CfProvider {
CfRestrictedFunc restricted;
} CfProvider;

/* Independent qualifications, kept apart from the tightening chain on purpose;
* cf-ext.h explains why they cannot share it. */
typedef struct CfShareProvider {
char *name;
CfShareGrantFunc grant;
} CfShareProvider;

static GList *providers = NULL; /* CfProvider*, registration order */
static GList *share_providers = NULL; /* CfShareProvider*, registration order */

gboolean
cf_ext_config_bool (const char *key)
Expand All @@ -45,7 +54,7 @@ cf_ext_database (void)
gboolean
cf_ext_active (void)
{
return providers != NULL;
return providers != NULL || share_providers != NULL;
}

void
Expand All @@ -64,6 +73,20 @@ cf_ext_register (const char *name,
seaf_message ("CloudFile: capability '%s' enabled.\n", name);
}

void
cf_ext_register_share_grant (const char *name, CfShareGrantFunc grant)
{
if (!grant)
return;

CfShareProvider *p = g_new0 (CfShareProvider, 1);
p->name = g_strdup (name);
p->grant = grant;

share_providers = g_list_append (share_providers, p);
seaf_message ("CloudFile: share qualification '%s' enabled.\n", name);
}

void
cf_ext_init (void)
{
Expand All @@ -79,6 +102,33 @@ cf_ext_init (void)
cf_acl_init ();
cf_fileop_test_init ();
cf_lock_init ();
cf_v2_share_init ();
}

char *
cf_ext_share_grant (const char *repo_id,
const char *path,
const char *user,
gboolean *unavailable)
{
if (unavailable)
*unavailable = FALSE;
if (!share_providers || !repo_id || !path || !user || !*user)
return NULL;

GList *ptr;
for (ptr = share_providers; ptr; ptr = ptr->next) {
CfShareProvider *p = ptr->data;
char *grant = p->grant (repo_id, path, user, unavailable);
if (grant)
return grant;
/* Its own authoritative data was unreadable: no later capability can
* answer for it, and "denied" would misreport an outage. */
if (unavailable && *unavailable)
return NULL;
}

return NULL;
}

char *
Expand Down
43 changes: 43 additions & 0 deletions common/cf-ext.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,49 @@ void cf_ext_register (const char *name,
CfDirentFilterFunc dirent_filter,
CfRestrictedFunc restricted);

/* ------------------------------------------- independent qualification */

/*
* A permission that exists *without* a native library grant.
*
* The chain above cannot express one, and must not be bent into doing so: it
* starts from @native_perm, every provider may only reduce it, and a NULL
* native permission ends it. A directory share is the opposite case -- it is
* the normal state of an account that is deliberately not a library member --
* so a share root would have to start the chain from nothing and *widen*,
* which no provider in that chain is audited for.
*
* Hence this second, explicitly named seam. It is consulted only after
* seafile_check_permission() returned nothing for @user, and it answers with
* a permission of its own for that exact path. It may never return "admin" and
* may never reach a path the capability did not qualify; NULL means "no
* independent qualification", and callers keep failing closed.
*
* Not registered (a stock CE deployment, or every switch off) means the native
* denial stands, exactly as before this seam existed.
*/
typedef char * (*CfShareGrantFunc) (const char *repo_id,
const char *path,
const char *user,
gboolean *unavailable);

/* Register an independent-qualification capability. */
void cf_ext_register_share_grant (const char *name, CfShareGrantFunc grant);

/*
* The first qualification any capability will vouch for, or NULL.
*
* @unavailable is optional and behaves as in cf_ext_check_permission: TRUE only
* when the capability that could have qualified the path could not read its own
* authoritative data, so the caller must answer "unavailable" rather than
* "denied". A capability that reports unavailable stops the search; a later
* one could not restore an answer the earlier one failed to compute.
*/
char *cf_ext_share_grant (const char *repo_id,
const char *path,
const char *user,
gboolean *unavailable);

/* ------------------------------------------------------------------ dispatch */

/*
Expand Down
52 changes: 38 additions & 14 deletions common/cf-v2-acl.c
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,29 @@ static size_t response_body(char *data, size_t size, size_t n, void *opaque)
return length;
}

static int authorize(struct _SeafileSession *session, json_t *request)
/*
* Transport only: one POST to the Hub's internal V2 check endpoint.
*
* Split out of authorize() so the directory-share qualification (cf-v2-share.c)
* reuses this channel instead of growing a second curl block with its own JWT
* lifetime rules -- two copies of an internal-auth handshake are one copy too
* many, and the one that drifts is always the less exercised one.
*
* Returns the HTTP status, or -1 when the channel itself is unusable: no key or
* url, JWT failure, transport error, oversized or truncated reply. @body is
* reset, and holds the reply on 200.
*/
long
cf_v2_acl_check_json(struct _SeafileSession *session, const char *payload, GString *body)
{
int result=-1;
long status=-1;
jwt_t *jwt=NULL;
CURL *curl=NULL;
struct curl_slist *headers=NULL;
char *token=NULL, *auth=NULL, *url=NULL, *payload=NULL;
GString *body=g_string_new(NULL);
json_t *reply=NULL;
if (!session->seahub_pk || !*session->seahub_pk || !session->seahub_url) goto done;
char *token=NULL, *auth=NULL, *url=NULL;
if (!session || !payload || !body) return -1;
g_string_truncate(body,0);
if (!session->seahub_pk || !*session->seahub_pk || !session->seahub_url) return -1;
if (jwt_new(&jwt) || jwt_add_grant_bool(jwt,"is_internal",TRUE) ||
jwt_add_grant_int(jwt,"exp",time(NULL)+60) ||
jwt_set_alg(jwt,JWT_ALG_HS256,(unsigned char *)session->seahub_pk,strlen(session->seahub_pk))) goto done;
Expand All @@ -158,8 +171,6 @@ static int authorize(struct _SeafileSession *session, json_t *request)
auth=g_strdup_printf("Authorization: Token %s",token);
headers=curl_slist_append(headers,auth);
headers=curl_slist_append(headers,"Content-Type: application/json");
payload=json_dumps(request,JSON_COMPACT);
if (!payload) goto done;
curl_easy_setopt(curl,CURLOPT_URL,url);
curl_easy_setopt(curl,CURLOPT_HTTPHEADER,headers);
curl_easy_setopt(curl,CURLOPT_POSTFIELDS,payload);
Expand All @@ -169,17 +180,30 @@ static int authorize(struct _SeafileSession *session, json_t *request)
curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,response_body);
curl_easy_setopt(curl,CURLOPT_WRITEDATA,body);
if (curl_easy_perform(curl)!=CURLE_OK) goto done;
long status=0;
if (curl_easy_getinfo(curl,CURLINFO_RESPONSE_CODE,&status)!=CURLE_OK || status!=200) goto done;
reply=json_loadb(body->str,body->len,JSON_REJECT_DUPLICATES,NULL);
if (json_is_true(json_object_get(reply,"allowed")) &&
!g_strcmp0(json_string_value(json_object_get(reply,"contract")),"cf-acl-v1")) result=0;
if (curl_easy_getinfo(curl,CURLINFO_RESPONSE_CODE,&status)!=CURLE_OK) status=-1;
done:
if (curl) curl_easy_cleanup(curl);
curl_slist_free_all(headers);
if (jwt) jwt_free(jwt);
g_free(token);g_free(auth);g_free(url);
return status;
}

static int authorize(struct _SeafileSession *session, json_t *request)
{
int result=-1;
char *payload=NULL;
GString *body=g_string_new(NULL);
json_t *reply=NULL;
payload=json_dumps(request,JSON_COMPACT);
if (!payload) goto done;
if (cf_v2_acl_check_json(session,payload,body)!=200) goto done;
reply=json_loadb(body->str,body->len,JSON_REJECT_DUPLICATES,NULL);
if (json_is_true(json_object_get(reply,"allowed")) &&
!g_strcmp0(json_string_value(json_object_get(reply,"contract")),"cf-acl-v1")) result=0;
done:
json_decref(reply);
g_free(token);g_free(auth);g_free(url);g_free(payload);g_string_free(body,TRUE);
g_free(payload);g_string_free(body,TRUE);
return result;
}

Expand Down
6 changes: 6 additions & 0 deletions common/cf-v2-acl.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@ int cf_v2_acl_final(SeafDBTrans *trans, struct _SeafileSession *session,
const char *repo_id);
int cf_v2_acl_final_scoped(SeafDBTrans *trans, struct _SeafileSession *session,
const char *repo_id, const char *old_id, const char *new_id, const char *actor);
/* Transport only: one internal-JWT POST to /cloudfile/acl/check/. Returns the
* HTTP status, or -1 when the channel is unusable. @body is reset and holds the
* reply on 200. The single implementation of that handshake; cf-v2-share.c
* consumes it rather than keeping a second copy. */
long cf_v2_acl_check_json(struct _SeafileSession *session, const char *payload,
GString *body);
#endif
Loading
Loading