tfrere HF Staff commited on
Commit
3616e87
·
1 Parent(s): ade54e4

fix: auth flow for publish and write access check

Browse files

- Add credentials: "include" to publish fetch so cookies are sent
- Rewrite checkWriteAccess: check Space owner match or org membership
instead of unreliable /settings endpoint probe
- Add debug logging for auth and publish failures

Made-with: Cursor

backend/src/auth.ts CHANGED
@@ -33,6 +33,7 @@ export async function resolveUser(
33
  const avatarUrl = info.avatarUrl || "";
34
 
35
  const canEdit = await checkWriteAccess(accessToken, name);
 
36
 
37
  return { name, fullName, avatarUrl, canEdit };
38
  } catch (err) {
@@ -43,34 +44,43 @@ export async function resolveUser(
43
 
44
  /**
45
  * Check if user has write access to the Space repo.
 
 
 
 
 
 
 
 
46
  * Falls back to false on any error.
47
  */
48
  async function checkWriteAccess(
49
  accessToken: string,
50
- _username: string
51
  ): Promise<boolean> {
52
  if (!SPACE_ID) return false;
53
 
54
  try {
 
 
 
 
 
 
55
  const res = await fetch(
56
- `https://huggingface.co/api/spaces/${SPACE_ID}`,
57
  { headers: { Authorization: `Bearer ${accessToken}` } }
58
  );
59
 
60
- if (!res.ok) return false;
61
-
62
- const data = (await res.json()) as Record<string, unknown>;
63
-
64
- if (data.private === false) {
65
- const authorOrg = SPACE_ID.split("/")[0];
66
- const res2 = await fetch(
67
- `https://huggingface.co/api/spaces/${SPACE_ID}/settings`,
68
- { headers: { Authorization: `Bearer ${accessToken}` } }
69
- );
70
- return res2.ok;
71
  }
72
 
73
- return true;
74
  } catch {
75
  return false;
76
  }
 
33
  const avatarUrl = info.avatarUrl || "";
34
 
35
  const canEdit = await checkWriteAccess(accessToken, name);
36
+ console.log(`[auth] user=${name} canEdit=${canEdit}`);
37
 
38
  return { name, fullName, avatarUrl, canEdit };
39
  } catch (err) {
 
44
 
45
  /**
46
  * Check if user has write access to the Space repo.
47
+ *
48
+ * Strategy:
49
+ * 1. If the user owns the Space namespace, grant access.
50
+ * 2. If the user belongs to the org that owns the Space, check via
51
+ * the /api/spaces/{id} endpoint which returns the user's role
52
+ * when authenticated (the "role" field is present for members).
53
+ * 3. For public Spaces, try the /settings endpoint as a write-access probe.
54
+ *
55
  * Falls back to false on any error.
56
  */
57
  async function checkWriteAccess(
58
  accessToken: string,
59
+ username: string
60
  ): Promise<boolean> {
61
  if (!SPACE_ID) return false;
62
 
63
  try {
64
+ const spaceOwner = SPACE_ID.split("/")[0];
65
+
66
+ // Owner of the Space namespace has write access
67
+ if (spaceOwner === username) return true;
68
+
69
+ // Check if user is a member of the org with write/admin role
70
  const res = await fetch(
71
+ `https://huggingface.co/api/organizations/${spaceOwner}/members`,
72
  { headers: { Authorization: `Bearer ${accessToken}` } }
73
  );
74
 
75
+ if (res.ok) {
76
+ const members = (await res.json()) as Array<{ user: string; role: string }>;
77
+ const member = members.find((m) => m.user === username);
78
+ if (member && (member.role === "write" || member.role === "admin")) {
79
+ return true;
80
+ }
 
 
 
 
 
81
  }
82
 
83
+ return false;
84
  } catch {
85
  return false;
86
  }
backend/src/server.ts CHANGED
@@ -145,7 +145,13 @@ app.post("/api/publish", async (req, res) => {
145
  if (oauthEnabled) {
146
  const token = extractToken(req.headers.cookie);
147
  const user = await resolveUser(token);
148
- if (!user || !user.canEdit) {
 
 
 
 
 
 
149
  res.status(403).json({ error: "Unauthorized: write access required" });
150
  return;
151
  }
 
145
  if (oauthEnabled) {
146
  const token = extractToken(req.headers.cookie);
147
  const user = await resolveUser(token);
148
+ if (!user) {
149
+ console.warn("[publish] no valid user from token, cookie present:", !!token);
150
+ res.status(403).json({ error: "Unauthorized: please log in first" });
151
+ return;
152
+ }
153
+ if (!user.canEdit) {
154
+ console.warn("[publish] user lacks write access:", user.name);
155
  res.status(403).json({ error: "Unauthorized: write access required" });
156
  return;
157
  }
frontend/src/App.tsx CHANGED
@@ -127,6 +127,7 @@ export default function App() {
127
  const res = await fetch("/api/publish", {
128
  method: "POST",
129
  headers: { "Content-Type": "application/json" },
 
130
  body: JSON.stringify({ docName }),
131
  });
132
  if (!res.ok) {
 
127
  const res = await fetch("/api/publish", {
128
  method: "POST",
129
  headers: { "Content-Type": "application/json" },
130
+ credentials: "include",
131
  body: JSON.stringify({ docName }),
132
  });
133
  if (!res.ok) {