Skip to content

ACLs on mapped subjects (like JS.cloud.API.>) don't work #6902

@shaunco

Description

@shaunco

Observed behavior

(I started to write this as a comment in nats-io/nats.go#1089 but ended up tracing it into nats-server, so putting it here instead)

It is not possible to grant only permissions for JetStream with the domain included, as JS.API.> is the real path and something like JS.cloud.API.> is the mapped path, but ACLs seem to only be validated against the real path. When trying to have very tightly controlled ACLs on both the hub side and for a leafnode, this leads to some really weird behavior (that I still haven't figured out how to work around).

When the messages come in to nats-server, this code maps the subject back:

if (c.kind == CLIENT || c.kind == LEAF) && c.in.flags.isSet(hasMappings) {
changed := c.selectMappedSubject()
if changed {
if trace {
c.traceInOp("MAPPING", []byte(fmt.Sprintf("%s -> %s", c.pa.mapped, c.pa.subject)))
}
// c.pa.subject is the subject the original is now mapped to.
mt.addSubjectMappingEvent(c.pa.subject)
}
}

and then

if c.perms != nil && (c.perms.pub.allow != nil || c.perms.pub.deny != nil) && !c.pubAllowedFullCheck(string(c.pa.subject), true, true) {
rejects the request based on c.pa.subject rather than c.pa.mapped.


If you change the ACLs in the conf file from the example below to remove the .cloud, the stream creation works ... but then if you create a mirror stream in a leafnode (domain=leaf, Mirror.Domain=cloud), the leafnode logs will keep getting debug logs like this during sync:

Not permitted to deliver to "$JS.API.CONSUMER.MSG.NEXT....
Not permitted to deliver to "$JS.API.CONSUMER.MSG.NEXT....
Not permitted to deliver to "$JS.API.DIRECT.GET....
Not permitted to deliver to "$JS.API.STREAM.PURGE....

Which is also strange since both the hub client code and the leafnode code were explicit about domains, and neither ever tried to use JS.API.>, but both are now running into the default leafnode deny of JS.API.>, despite the attempts to properly use fully qualified JS subjects on both sides of the mirror sync.


So, while I understand that ACLs really should apply to the actual subject rather than a mapped subject, it seems like there should be some special case here for JetStream domains since all the docs encourage using the JS.cloud.API.> style (WithDomain) when a leafnode is involved.

... probably also the root of #6901

Expected behavior

I would actually expect the fully qualified JS.cloud.API to be the real subject and JS.API to be the mapping, since the fully qualified is used for imports/exports, when a leafnode is in play, and seems to be more specific ... but, even if the mapping direction stays as is, I would expect ACLs to work on JetStream subjects that include the domain.

Server and client version

  • nats.go v1.42.0
  • server v2.11.3

Host environment

No response

Steps to reproduce

  1. Give a client connected to the hub (with domain=cloud) only access to JS.cloud.API (specifically no access to the unnamed JS.API.>):
     # cloud.conf
     server_name: "cloud"
     listen: "0.0.0.0:4222"
     http: 8222
     max_payload: 8388608
    
     leafnodes {
       no_advertise: true
       port: 7422
     }
    
     websocket {
       port: 7443
       no_tls: true
     }
    
     jetstream {
       store_dir: "./cloud_data"
       domain: "cloud"
       max_memory_store: 512MB
       max_file_store: 2GB
     }
    
     accounts {
       SYS: {
         users: [
           {
             user:     "admin"
             password: "admin"
             permissions {
               publish:   [">"]
               subscribe: [">"]
             }
           }
         ]
       }
    
       TEST: {
         jetstream: enabled
    
         users: [
           {
             user:     "test"
             password: "test"
             permissions {
               publish: [
                 "$JS.cloud.API.STREAM.CREATE.>",
                 "$JS.cloud.API.STREAM.INFO",
                 "$JS.cloud.API.STREAM.INFO.>",
               ]
               subscribe: [
                 "_INBOX.>",
               ]
               allow_responses: true
             }
           },
         ]
       }
     }
    
     system_account: "SYS"
  2. Try to create a stream with the domain set:
     import (
         "github.com/nats-io/nats.go"
         "github.com/nats-io/nats.go/jetstream"
     )
    
     func main() {
     	nc, _ := nats.Connect("nats://test:test@localhost:4222", nats.Name("test"))
             defer nc.Drain()
             js, _ := jetstream.NewWithDomain(nc, "cloud")
     	if _, err := js.Stream(ctx, "TEST_STREAM"); err != nil {
     		if _, err := js.CreateStream(ctx, jetstream.StreamConfig{
     			Name:        "TEST_STREAM",
     			Subjects:    []string{"my.test.subject"},
     			Retention:   jetstream.WorkQueuePolicy,
     			Replicas:    1,
     			Storage:     jetstream.FileStorage,
     			Compression: jetstream.S2Compression,
     		}); err != nil {
     			panic(err)
     		}
     	}
         }
    The nats.go debug console will show:
    nats: permissions violation: Permissions Violation for Publish to "$JS.API.STREAM.INFO.TEST_STREAM" on connection [13]
    nats: permissions violation: Permissions Violation for Publish to "$JS.API.STREAM.CREATE.TEST_STREAM" on connection [13]
    The nats-server debug console will show:
    Publish Violation - User "test", Subject "$JS.API.STREAM.INFO.TEST_STREAM"
    Publish Violation - User "test", Subject "$JS.API.STREAM.INFO.TEST_STREAM"
  3. Change jetstream.NewWithDomain(nc, "cloud") to jetstream.NewWithAPIPrefix(nc, "$JS.cloud.API") ... note the same errors.

Metadata

Metadata

Assignees

Labels

defectSuspected defect such as a bug or regressionstaleThis issue has had no activity in a while

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions