Merge pull request #3719 from nats-io/auth_callout

Authorization Callouts
This commit is contained in:
Derek Collison
2023-01-03 15:34:10 -08:00
committed by GitHub
15 changed files with 1763 additions and 52 deletions

View File

@@ -1,4 +1,4 @@
// Copyright 2012-2022 The NATS Authors
// Copyright 2012-2023 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -262,7 +262,7 @@ func (s *Server) configureAuthorization() {
} else if opts.Nkeys != nil || opts.Users != nil {
s.nkeys, s.users = s.buildNkeysAndUsersFromOptions(opts.Nkeys, opts.Users)
s.info.AuthRequired = true
} else if opts.Username != "" || opts.Authorization != "" {
} else if opts.Username != _EMPTY_ || opts.Authorization != _EMPTY_ {
s.info.AuthRequired = true
} else {
s.users = nil
@@ -274,6 +274,27 @@ func (s *Server) configureAuthorization() {
s.wsConfigAuth(&opts.Websocket)
// And for mqtt config
s.mqttConfigAuth(&opts.MQTT)
// Check for server configured auth callouts.
if opts.AuthCallout != nil {
// Make sure we have a valid account and auth_users.
_, err := s.lookupAccount(opts.AuthCallout.Account)
if err != nil {
s.Errorf("Authorization callout account %q not valid", opts.AuthCallout.Account)
}
for _, u := range opts.AuthCallout.AuthUsers {
// Check for user in users and nkeys since this is server config.
var found bool
if len(s.users) > 0 {
_, found = s.users[u]
} else if len(s.nkeys) > 0 && !found {
_, found = s.nkeys[u]
}
if !found {
s.Errorf("Authorization callout user %q not valid: %v", u, err)
}
}
}
}
// Takes the given slices of NkeyUser and User options and build
@@ -547,7 +568,7 @@ func processUserPermissionsTemplate(lim jwt.UserPermissionLimits, ujwt *jwt.User
return lim, nil
}
func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) bool {
func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (authorized bool) {
var (
nkey *NkeyUser
juc *jwt.UserClaims
@@ -557,6 +578,70 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo
err error
ao bool // auth override
)
// Check if we have auth callouts enabled at the server level or in the bound account.
defer func() {
// Default reason
reason := AuthenticationViolation.String()
// No-op
if juc == nil && opts.AuthCallout == nil {
if !authorized {
s.sendAccountAuthErrorEvent(c, c.acc, reason)
}
return
}
// We have a juc defined here, check account.
if juc != nil && !acc.hasExternalAuth() {
if !authorized {
s.sendAccountAuthErrorEvent(c, c.acc, reason)
}
return
}
// We have auth callout set here.
var skip bool
// Check if we are on the list of auth_users.
userID := c.getRawAuthUser()
if juc != nil {
skip = acc.isExternalAuthUser(userID)
} else {
for _, u := range opts.AuthCallout.AuthUsers {
if userID == u {
skip = true
break
}
}
}
// If we are here we have an auth callout defined and we have failed auth so far
// so we will callout to our auth backend for processing.
if !skip {
authorized, reason = s.processClientOrLeafCallout(c, opts)
}
// Check if we are authorized and in the auth callout account, and if so add in deny publish permissions for the auth subject.
if authorized {
var authAccountName string
if juc == nil && opts.AuthCallout != nil {
authAccountName = opts.AuthCallout.Account
} else if juc != nil {
authAccountName = acc.Name
}
c.mu.Lock()
if c.acc != nil && c.acc.Name == authAccountName {
c.mergeDenyPermissions(pub, []string{AuthCalloutSubject})
}
c.mu.Unlock()
} else {
// If we are here we failed external authorization.
// Send an account scoped event. Server config mode acc will be nil,
// so lookup the auth callout assigned account, that is where this will be sent.
if acc == nil {
acc, _ = s.lookupAccount(opts.AuthCallout.Account)
}
s.sendAccountAuthErrorEvent(c, acc, reason)
}
}()
s.mu.Lock()
authRequired := s.info.AuthRequired
if !authRequired {
@@ -811,7 +896,7 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo
return false
}
if juc.BearerToken && acc.failBearer() {
c.Debugf("Account does not allow bearer token")
c.Debugf("Account does not allow bearer tokens")
return false
}
// skip validation of nonce when presented with a bearer token
@@ -1036,7 +1121,7 @@ func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool {
// https://github.com/golang/go/issues/12342
dn, err := ldap.FromRawCertSubject(cert.RawSubject)
if err == nil {
if match, ok := fn("", dn, false); ok {
if match, ok := fn(_EMPTY_, dn, false); ok {
c.Debugf("Using DistinguishedNameMatch for auth [%q]", match)
return true
}
@@ -1119,22 +1204,22 @@ func (s *Server) isRouterAuthorized(c *client) bool {
if opts.Cluster.TLSMap || opts.Cluster.TLSCheckKnownURLs {
return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN, isDNSAltName bool) (string, bool) {
if user == "" {
return "", false
if user == _EMPTY_ {
return _EMPTY_, false
}
if opts.Cluster.TLSCheckKnownURLs && isDNSAltName {
if dnsAltNameMatches(dnsAltNameLabels(user), opts.Routes) {
return "", true
return _EMPTY_, true
}
}
if opts.Cluster.TLSMap && opts.Cluster.Username == user {
return "", true
return _EMPTY_, true
}
return "", false
return _EMPTY_, false
})
}
if opts.Cluster.Username == "" {
if opts.Cluster.Username == _EMPTY_ {
return true
}
@@ -1155,25 +1240,25 @@ func (s *Server) isGatewayAuthorized(c *client) bool {
// Check whether TLS map is enabled, otherwise use single user/pass.
if opts.Gateway.TLSMap || opts.Gateway.TLSCheckKnownURLs {
return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN, isDNSAltName bool) (string, bool) {
if user == "" {
return "", false
if user == _EMPTY_ {
return _EMPTY_, false
}
if opts.Gateway.TLSCheckKnownURLs && isDNSAltName {
labels := dnsAltNameLabels(user)
for _, gw := range opts.Gateway.Gateways {
if gw != nil && dnsAltNameMatches(labels, gw.URLs) {
return "", true
return _EMPTY_, true
}
}
}
if opts.Gateway.TLSMap && opts.Gateway.Username == user {
return "", true
return _EMPTY_, true
}
return "", false
return _EMPTY_, false
})
}
if opts.Gateway.Username == "" {
if opts.Gateway.Username == _EMPTY_ {
return true
}