Skip to content

Commit 6137efa

Browse files
Merge pull request #2270 from h2zh/prefix-validation-fix
Properly handle the trailing`/` in namespace registration and advertisement
2 parents eaf759a + 8e52103 commit 6137efa

6 files changed

Lines changed: 153 additions & 101 deletions

File tree

director/director.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,11 @@ func registerServerAd(engineCtx context.Context, ctx *gin.Context, sType server_
993993
adV2 = server_structs.ConvertOriginAdV1ToV2(ad)
994994
}
995995

996+
// Check every namespace path to strip the trailing slash
997+
for i := range adV2.Namespaces {
998+
adV2.Namespaces[i].Path = server_utils.RemoveTrailingSlash(adV2.Namespaces[i].Path)
999+
}
1000+
9961001
// Filter the advertised prefixes in the cache server ad
9971002
// based on the allowed prefixes for caches data.
9981003
if sType == server_structs.CacheType {
@@ -1061,7 +1066,7 @@ func registerServerAd(engineCtx context.Context, ctx *gin.Context, sType server_
10611066
// Verify server registration
10621067
token := strings.TrimPrefix(tokens[0], "Bearer ")
10631068

1064-
registryPrefix := adV2.RegistryPrefix
1069+
registryPrefix := server_utils.RemoveTrailingSlash(adV2.RegistryPrefix)
10651070
verifyServer := true
10661071
if registryPrefix == "" {
10671072
if sType == server_structs.OriginType {

registry/registry.go

Lines changed: 102 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -277,124 +277,127 @@ func keySignChallengeCommit(ctx *gin.Context, data *registrationData) (bool, map
277277
serverPubkey := serverPrivateKey.PublicKey
278278
serverVerified := verifySignature(serverPayload, serverSignature, &serverPubkey)
279279

280-
if clientVerified && serverVerified {
281-
log.Debug("Registering namespace ", data.Prefix)
280+
if !(clientVerified && serverVerified) {
281+
return false, nil, errors.Errorf("Unable to verify the client's public key, or an encountered an error with its own: "+
282+
"server verified:%t, client verified:%t", serverVerified, clientVerified)
283+
}
282284

283-
// Check if prefix exists before doing anything else
284-
exists, err := namespaceExistsByPrefix(data.Prefix)
285-
if err != nil {
286-
log.Errorf("Failed to check if namespace already exists: %v", err)
287-
return false, nil, errors.Wrap(err, "Server encountered an error checking if namespace already exists")
288-
}
289-
if exists {
290-
returnMsg := map[string]interface{}{
291-
"message": fmt.Sprintf("The prefix %s is already registered -- nothing else to do!", data.Prefix),
292-
}
293-
log.Infof("Skipping registration of prefix %s because it's already registered.", data.Prefix)
294-
return false, returnMsg, nil
295-
}
285+
// Ensure the user input prefix doesn't contain any invalid characters.
286+
// Also remove the trailing slash if it exists.
287+
reqPrefix, err := validatePrefix(data.Prefix)
288+
if err != nil {
289+
err = errors.Wrapf(err, "requested namespace %s failed validation", data.Prefix)
290+
log.Errorln(err)
291+
return false, nil, badRequestError{Message: err.Error()}
292+
}
293+
data.Prefix = reqPrefix
296294

297-
reqPrefix, err := validatePrefix(data.Prefix)
298-
if err != nil {
299-
err = errors.Wrapf(err, "Requested namespace %s failed validation", data.Prefix)
300-
log.Errorln(err)
301-
return false, nil, badRequestError{Message: err.Error()}
302-
}
303-
data.Prefix = reqPrefix
295+
log.Debug("Registering namespace ", data.Prefix)
304296

305-
inTopo, topoNss, valErr, sysErr := validateKeyChaining(reqPrefix, key)
306-
if valErr != nil {
307-
log.Errorln(err)
308-
return false, nil, permissionDeniedError{Message: valErr.Error()}
309-
}
310-
if sysErr != nil {
311-
log.Errorln(err)
312-
return false, nil, sysErr
297+
// Check if prefix exists before doing anything else
298+
exists, err := namespaceExistsByPrefix(data.Prefix)
299+
if err != nil {
300+
log.Errorf("Failed to check if namespace already exists: %v", err)
301+
return false, nil, errors.Wrap(err, "Server encountered an error checking if namespace already exists")
302+
}
303+
if exists {
304+
returnMsg := map[string]interface{}{
305+
"message": fmt.Sprintf("The prefix %s is already registered -- nothing else to do!", data.Prefix),
313306
}
307+
log.Infof("Skipping registration of prefix %s because it's already registered.", data.Prefix)
308+
return false, returnMsg, nil
309+
}
310+
311+
inTopo, topoNss, valErr, sysErr := validateKeyChaining(data.Prefix, key)
312+
if valErr != nil {
313+
log.Errorln(err)
314+
return false, nil, permissionDeniedError{Message: valErr.Error()}
315+
}
316+
if sysErr != nil {
317+
log.Errorln(err)
318+
return false, nil, sysErr
319+
}
320+
321+
var ns server_structs.Namespace
322+
ns.Prefix = data.Prefix
314323

315-
var ns server_structs.Namespace
316-
ns.Prefix = data.Prefix
324+
pubkeyData, err := json.Marshal(data.Pubkey)
325+
if err != nil {
326+
return false, nil, errors.Wrapf(err, "Failed to convert public key from json to string format for the prefix %s", ns.Prefix)
327+
}
328+
ns.Pubkey = string(pubkeyData)
329+
ns.Identity = data.Identity
330+
ns.AdminMetadata.SiteName = data.SiteName
317331

318-
pubkeyData, err := json.Marshal(data.Pubkey)
332+
if data.Identity != "" {
333+
idMap := map[string]interface{}{}
334+
err := json.Unmarshal([]byte(data.Identity), &idMap)
319335
if err != nil {
320-
return false, nil, errors.Wrapf(err, "Failed to convert public key from json to string format for the prefix %s", ns.Prefix)
336+
log.Errorln("Failed to decode non-empty Identity field:", err)
337+
return false, nil, err
321338
}
322-
ns.Pubkey = string(pubkeyData)
323-
ns.Identity = data.Identity
324-
ns.AdminMetadata.SiteName = data.SiteName
325-
326-
if data.Identity != "" {
327-
idMap := map[string]interface{}{}
328-
err := json.Unmarshal([]byte(data.Identity), &idMap)
329-
if err != nil {
330-
log.Errorln("Failed to decode non-empty Identity field:", err)
331-
return false, nil, err
332-
}
333-
sub, ok := idMap["sub"]
339+
sub, ok := idMap["sub"]
340+
if ok {
341+
val, ok := sub.(string)
334342
if ok {
335-
val, ok := sub.(string)
336-
if ok {
337-
ns.AdminMetadata.UserID = val
338-
}
343+
ns.AdminMetadata.UserID = val
339344
}
340-
if inTopo {
341-
topoNssStr := GetTopoPrefixString(topoNss)
342-
ns.AdminMetadata.Description = fmt.Sprintf("[ Attention: A superspace or subspace of this prefix exists in OSDF topology: %s ] ", topoNssStr)
343-
}
344-
userName, ok := idMap["name"]
345+
}
346+
if inTopo {
347+
topoNssStr := GetTopoPrefixString(topoNss)
348+
ns.AdminMetadata.Description = fmt.Sprintf("[ Attention: A superspace or subspace of this prefix exists in OSDF topology: %s ] ", topoNssStr)
349+
}
350+
userName, ok := idMap["name"]
351+
if ok {
352+
val, ok := userName.(string)
345353
if ok {
346-
val, ok := userName.(string)
347-
if ok {
348-
ns.AdminMetadata.Description += "User name: " + val + " "
349-
}
354+
ns.AdminMetadata.Description += "User name: " + val + " "
350355
}
351-
email, ok := idMap["email"]
356+
}
357+
email, ok := idMap["email"]
358+
if ok {
359+
val, ok := email.(string)
352360
if ok {
353-
val, ok := email.(string)
354-
if ok {
355-
ns.AdminMetadata.Description += "User email: " + val + " This is a namespace registration from Pelican CLI with OIDC authentication. Certain fields may not be populated"
356-
}
357-
}
358-
} else {
359-
// This is either a registration from CLI without --with-identity flag or
360-
// an automated registration from origin or cache
361-
ns.AdminMetadata.Description = "This is a namespace registration from Pelican CLI or an automated registration. Certain fields may not be populated"
362-
363-
// If the namespace is in the topology, we require identity information to register a Pelican namespace
364-
// for verification purpose
365-
if inTopo {
366-
return false,
367-
nil,
368-
permissionDeniedError{Message: fmt.Sprintf("A superspace or subspace of this namespace %s already exists in the OSDF topology: %s. "+
369-
"To register a Pelican equivalence, you need to present your identity. "+
370-
"If you are registering through Pelican CLI, try again with the flag '--with-identity' enabled. "+
371-
"If this is an auto-registration from a Pelican origin or cache server, "+
372-
"register your namespace or server through the Pelican registry website at %s instead.",
373-
ns.Prefix,
374-
GetTopoPrefixString(topoNss),
375-
registryUrl)}
361+
ns.AdminMetadata.Description += "User email: " + val + " This is a namespace registration from Pelican CLI with OIDC authentication. Certain fields may not be populated"
376362
}
377363
}
364+
} else {
365+
// This is either a registration from CLI without --with-identity flag or
366+
// an automated registration from origin or cache
367+
ns.AdminMetadata.Description = "This is a namespace registration from Pelican CLI or an automated registration. Certain fields may not be populated"
368+
369+
// If the namespace is in the topology, we require identity information to register a Pelican namespace
370+
// for verification purpose
371+
if inTopo {
372+
return false,
373+
nil,
374+
permissionDeniedError{Message: fmt.Sprintf("A superspace or subspace of this namespace %s already exists in the OSDF topology: %s. "+
375+
"To register a Pelican equivalence, you need to present your identity. "+
376+
"If you are registering through Pelican CLI, try again with the flag '--with-identity' enabled. "+
377+
"If this is an auto-registration from a Pelican origin or cache server, "+
378+
"register your namespace or server through the Pelican registry website at %s instead.",
379+
ns.Prefix,
380+
GetTopoPrefixString(topoNss),
381+
registryUrl)}
382+
}
383+
}
378384

379-
// Overwrite status to Pending to filter malicious request
380-
ns.AdminMetadata.Status = server_structs.RegPending
385+
// Overwrite status to Pending to filter malicious request
386+
ns.AdminMetadata.Status = server_structs.RegPending
381387

382-
err = AddNamespace(&ns)
383-
if err != nil {
384-
return false, nil, errors.Wrapf(err, "Failed to add the prefix %q to the database", ns.Prefix)
385-
} else {
386-
msg := fmt.Sprintf("Prefix %s successfully registered", ns.Prefix)
387-
if inTopo {
388-
msg = fmt.Sprintf("Prefix %s successfully registered. Note that there is an existing superspace or subspace of the namespace in the OSDF topology: %s. The registry admin will review your request and approve your namespace if this is expected.", ns.Prefix, GetTopoPrefixString(topoNss))
389-
}
390-
return true, map[string]interface{}{
391-
"message": msg,
392-
}, nil
393-
}
388+
err = AddNamespace(&ns)
389+
if err != nil {
390+
return false, nil, errors.Wrapf(err, "Failed to add the prefix %q to the database", ns.Prefix)
394391
} else {
395-
return false, nil, errors.Errorf("Unable to verify the client's public key, or an encountered an error with its own: "+
396-
"server verified:%t, client verified:%t", serverVerified, clientVerified)
392+
msg := fmt.Sprintf("Prefix %s successfully registered", ns.Prefix)
393+
if inTopo {
394+
msg = fmt.Sprintf("Prefix %s successfully registered. Note that there is an existing superspace or subspace of the namespace in the OSDF topology: %s. The registry admin will review your request and approve your namespace if this is expected.", ns.Prefix, GetTopoPrefixString(topoNss))
395+
}
396+
return true, map[string]interface{}{
397+
"message": msg,
398+
}, nil
397399
}
400+
398401
}
399402

400403
// Handle the namespace registration with nonce generation and verification, regardless of

registry/registry_validation.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func validatePrefix(nspath string) (string, error) {
5555
result := ""
5656
for _, component := range components {
5757
if len(component) == 0 {
58-
continue
58+
continue // This can remove the trailing '/' in nspath
5959
} else if component == "." {
6060
return "", errors.New("Path component cannot be '.'")
6161
} else if component == ".." {

server_utils/origin.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,20 @@ func (b *BaseOrigin) validateExtra(*OriginExport, int) error {
260260
}
261261
func (b *BaseOrigin) mapSingleExtra() {}
262262

263+
// Remove the trailing '/' in the prefix, if it exists.
264+
// If the prefix is '/', leave it alone.
265+
func RemoveTrailingSlash(prefix string) string {
266+
result := prefix
267+
if prefix != "/" {
268+
result = strings.TrimSuffix(prefix, "/")
269+
}
270+
if result != prefix {
271+
// We log this as a warning because it may be a sign of a misconfiguration
272+
log.Warningf("Stripped trailing '/' from prefix '%s' before export", result)
273+
}
274+
return result
275+
}
276+
263277
// Since Federation Prefixes get treated like POSIX filepaths by XRootD and other services, we need to
264278
// validate them to ensure funky things don't ensue.
265279
// Note that this isn't a part of the origin interface because it's not meant to be overridden -- _every_ origin
@@ -416,6 +430,10 @@ func (b *BaseOrigin) validateExports(o Origin) (err error) {
416430
// Note that we assume we've already populated the origin export list
417431
for i := range b.Exports { // validateExtra may update some parts of the export, so we need the index.
418432
e := &b.Exports[i]
433+
434+
// strip the trailing '/' so that "/prefix" and "/prefix/" are treated identically
435+
e.FederationPrefix = RemoveTrailingSlash(e.FederationPrefix)
436+
419437
// all fed prefixes are validated the same way -- no way to override this one!
420438
if err = validateFederationPrefix(e.FederationPrefix); err != nil {
421439
return

server_utils/origin_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ var (
4242
//go:embed resources/posix-origins/multi-export-valid.yml
4343
multiExportValidConfig string
4444

45+
//go:embed resources/posix-origins/multi-export-trailing-slash.yml
46+
multiExportTrailingSlashConfig string
47+
4548
//go:embed resources/posix-origins/single-export-block.yml
4649
singleExportBlockConfig string
4750

@@ -207,6 +210,16 @@ func TestGetExports(t *testing.T) {
207210
assert.Equal(t, expectedExport2, exports[1])
208211
})
209212

213+
t.Run("testTrailingSlashRemovalPosix", func(t *testing.T) {
214+
defer ResetTestState()
215+
exports := setup(t, multiExportTrailingSlashConfig, false)
216+
217+
// Both trailing-slash prefixes should have been trimmed
218+
assert.Len(t, exports, 2)
219+
assert.Equal(t, "/first/namespace", exports[0].FederationPrefix)
220+
assert.Equal(t, "/", exports[1].FederationPrefix)
221+
})
222+
210223
t.Run("testExportVolumesValid", func(t *testing.T) {
211224
defer ResetTestState()
212225
exports := setup(t, exportVolumesValidConfig, false)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Origin export configuration to test if the Director can strip trailing slashes
2+
# in the advertising prefixes
3+
4+
Origin:
5+
StorageType: "posix"
6+
EnableDirectReads: true
7+
Exports:
8+
- StoragePrefix: /foo
9+
FederationPrefix: /first/namespace/
10+
Capabilities: ["PublicReads", "Writes"]
11+
- StoragePrefix: /bar
12+
FederationPrefix: /
13+
Capabilities: ["Reads"]

0 commit comments

Comments
 (0)