mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-19 15:11:36 +00:00
fix(dns): spell out the OVH right that lists zones
OVH matches access rules per exact path: a `GET /domain/zone/*` rule grants the
subtree but not the bare `GET /domain/zone` that listZones and testConnection
call. Verified against a live account with a consumer key carrying that single
wildcard rule:
GET /domain/zone -> 403 This call has not been granted
GET /domain/zone/ -> 200
GET /domain/zone/{zone}/record -> 200
The form only asked for rights on `/domain/zone/*`, so a token created by
following it could not list zones at all, and the failure surfaced as a bare
"This call has not been granted" that points nowhere.
The hint now lists the five rights verbatim, and a token missing the root one
gets an error that names it instead of echoing OVH's message.
Reported by @narcisonunez on #5258.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -149,14 +149,23 @@ describe("ovhClient.listZones", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("propagates the API error message", async () => {
|
||||
it("names the missing root right when OVH refuses the zone listing", async () => {
|
||||
const cfg = freshConfig();
|
||||
mockApi(ovhError("This call has not been granted", 403));
|
||||
|
||||
// A `GET /domain/zone/*` rule does not cover the bare `GET /domain/zone`,
|
||||
// so the raw OVH message would send users looking in the wrong place.
|
||||
await expect(ovhClient.listZones(cfg)).rejects.toThrow(
|
||||
"This call has not been granted",
|
||||
/missing the `GET \/domain\/zone` right/,
|
||||
);
|
||||
});
|
||||
|
||||
it("propagates the API error message", async () => {
|
||||
const cfg = freshConfig();
|
||||
mockApi(ovhError("Invalid signature", 403));
|
||||
|
||||
await expect(ovhClient.listZones(cfg)).rejects.toThrow("Invalid signature");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ovhClient.listRecords", () => {
|
||||
|
||||
@@ -488,10 +488,22 @@ export const HandleDnsProvider = ({ dnsProviderId }: Props) => {
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Create the three keys at once on api.ovh.com/createToken
|
||||
with the <code>GET</code>, <code>POST</code>,{" "}
|
||||
<code>PUT</code> and <code>DELETE</code> rights on{" "}
|
||||
<code>/domain/zone/*</code>.
|
||||
Create the three keys at once on
|
||||
api.ovh.com/createToken, with exactly these five rights:
|
||||
<br />
|
||||
<code>GET /domain/zone</code>
|
||||
<br />
|
||||
<code>GET /domain/zone/*</code>
|
||||
<br />
|
||||
<code>POST /domain/zone/*</code>
|
||||
<br />
|
||||
<code>PUT /domain/zone/*</code>
|
||||
<br />
|
||||
<code>DELETE /domain/zone/*</code>
|
||||
<br />
|
||||
The first one lists your zones and has to be granted on
|
||||
its own: OVH matches rights per exact path, so{" "}
|
||||
<code>/domain/zone/*</code> does not cover it.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -209,9 +209,29 @@ const recordBody = (
|
||||
...(record.ttl === undefined ? {} : { ttl: record.ttl }),
|
||||
});
|
||||
|
||||
// OVH grants access per exact path: a `/domain/zone/*` rule covers the subtree
|
||||
// but not the bare `/domain/zone` listing, which needs its own rule. That is an
|
||||
// easy one to leave out of a token, so say so plainly rather than surfacing a
|
||||
// bare "This call has not been granted".
|
||||
const listZoneNames = async (config: OvhConfig) => {
|
||||
try {
|
||||
return await ovhFetch<string[]>(config, "/domain/zone");
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("has not been granted")
|
||||
) {
|
||||
throw new Error(
|
||||
"OVH: the credentials are missing the `GET /domain/zone` right, which lists your zones. A `GET /domain/zone/*` rule does not cover it — add the rule without the wildcard as well.",
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const ovhClient: DnsClient<OvhConfig> = {
|
||||
async listZones(config) {
|
||||
const zones = await ovhFetch<string[]>(config, "/domain/zone");
|
||||
const zones = await listZoneNames(config);
|
||||
return zones.map((zone) => ({ id: zone, name: zone }));
|
||||
},
|
||||
|
||||
@@ -318,6 +338,6 @@ export const ovhClient: DnsClient<OvhConfig> = {
|
||||
},
|
||||
|
||||
async testConnection(config) {
|
||||
await ovhFetch(config, "/domain/zone");
|
||||
await listZoneNames(config);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user