Skip to content

Commit

Permalink
Merge pull request #179 from nats-io/auth_callout
Browse files Browse the repository at this point in the history
Add in support for external authorization for accounts.
  • Loading branch information
derekcollison authored Dec 1, 2022
2 parents 4a8a732 + f490b90 commit ca3be3c
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 12 deletions.
64 changes: 52 additions & 12 deletions v2/account_claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ import (
const NoLimit = -1

type AccountLimits struct {
Imports int64 `json:"imports,omitempty"` // Max number of imports
Exports int64 `json:"exports,omitempty"` // Max number of exports
WildcardExports bool `json:"wildcards,omitempty"` // Are wildcards allowed in exports
Imports int64 `json:"imports,omitempty"` // Max number of imports
Exports int64 `json:"exports,omitempty"` // Max number of exports
WildcardExports bool `json:"wildcards,omitempty"` // Are wildcards allowed in exports
DisallowBearer bool `json:"disallow_bearer,omitempty"` // User JWT can't be bearer token
Conn int64 `json:"conn,omitempty"` // Max number of active connections
LeafNodeConn int64 `json:"leaf,omitempty"` // Max number of active leaf node connections
Conn int64 `json:"conn,omitempty"` // Max number of active connections
LeafNodeConn int64 `json:"leaf,omitempty"` // Max number of active leaf node connections
}

// IsUnlimited returns true if all limits are unlimited
Expand Down Expand Up @@ -167,15 +167,54 @@ func (a *Account) AddMapping(sub Subject, to ...WeightedMapping) {
a.Mappings[sub] = to
}

// Enable external authorization for account users.
// AuthUsers are those users specified to bypass the authorization callout and should be used for the authorization service itself.
// AllowedAccounts specifies which accounts, if any, that the authorization service can bind an authorized user to.
// The authorization response, a user JWT, will still need to be signed by the correct account.
type ExternalAuthorization struct {
AuthUsers StringList `json:"auth_users"`
AllowedAccounts StringList `json:"allowed_accounts,omitempty"`
}

func (ac *ExternalAuthorization) IsEnabled() bool {
return len(ac.AuthUsers) > 0
}

// Helper function to determine if external authorization is enabled.
func (a *Account) HasExternalAuthorization() bool {
return a.Authorization.IsEnabled()
}

// Helper function to setup external authorization.
func (a *Account) EnableExternalAuthorization(users ...string) {
a.Authorization.AuthUsers.Add(users...)
}

func (ac *ExternalAuthorization) Validate(vr *ValidationResults) {
// Make sure users are all valid user nkeys.
// Make sure allowed accounts are all valid account nkeys.
for _, u := range ac.AuthUsers {
if !nkeys.IsValidPublicUserKey(u) {
vr.AddError("AuthUser %q is not a valid user public key", u)
}
}
for _, a := range ac.AllowedAccounts {
if !nkeys.IsValidPublicAccountKey(a) {
vr.AddError("Account %q is not a valid account public key", a)
}
}
}

// Account holds account specific claims data
type Account struct {
Imports Imports `json:"imports,omitempty"`
Exports Exports `json:"exports,omitempty"`
Limits OperatorLimits `json:"limits,omitempty"`
SigningKeys SigningKeys `json:"signing_keys,omitempty"`
Revocations RevocationList `json:"revocations,omitempty"`
DefaultPermissions Permissions `json:"default_permissions,omitempty"`
Mappings Mapping `json:"mappings,omitempty"`
Imports Imports `json:"imports,omitempty"`
Exports Exports `json:"exports,omitempty"`
Limits OperatorLimits `json:"limits,omitempty"`
SigningKeys SigningKeys `json:"signing_keys,omitempty"`
Revocations RevocationList `json:"revocations,omitempty"`
DefaultPermissions Permissions `json:"default_permissions,omitempty"`
Mappings Mapping `json:"mappings,omitempty"`
Authorization ExternalAuthorization `json:"authorization,omitempty"`
Info
GenericFields
}
Expand All @@ -187,6 +226,7 @@ func (a *Account) Validate(acct *AccountClaims, vr *ValidationResults) {
a.Limits.Validate(vr)
a.DefaultPermissions.Validate(vr)
a.Mappings.Validate(vr)
a.Authorization.Validate(vr)

if !a.Limits.IsEmpty() && a.Limits.Imports >= 0 && int64(len(a.Imports)) > a.Limits.Imports {
vr.AddError("the account contains more imports than allowed by the operator")
Expand Down
48 changes: 48 additions & 0 deletions v2/account_claims_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -697,3 +697,51 @@ func TestAccountMapping(t *testing.T) { // don't block encoding!!!
t.Fatal("Expected errors due to wildcard in weighted mapping")
}
}

func TestAccountExternalAuthorization(t *testing.T) {
akp := createAccountNKey(t)
apk := publicKey(akp, t)

account := NewAccountClaims(apk)
vr := &ValidationResults{}

ukp := createUserNKey(t)
account.EnableExternalAuthorization(publicKey(ukp, t))
account.Validate(vr)
if !vr.IsEmpty() {
t.Fatal("Expected no errors")
}

akp2 := createAccountNKey(t)
account.Authorization.AllowedAccounts.Add(publicKey(akp2, t))
account.Validate(vr)
if !vr.IsEmpty() {
t.Fatal("Expected no errors")
}

if !account.HasExternalAuthorization() {
t.Fatalf("Expected to have authorization enabled")
}

vr = &ValidationResults{}
account.Authorization = ExternalAuthorization{}

account.Authorization.AuthUsers.Add("Z")
account.Validate(vr)
if vr.IsEmpty() || !vr.IsBlocking(false) {
t.Fatalf("Expected blocking error on bad auth user")
}

vr = &ValidationResults{}
account.Authorization = ExternalAuthorization{}
account.Authorization.AllowedAccounts.Add("Z")
account.Validate(vr)
if vr.IsEmpty() || !vr.IsBlocking(false) {
t.Fatalf("Expected blocking error on bad allowed account")
}

account.Authorization = ExternalAuthorization{}
if account.Authorization.IsEnabled() {
t.Fatalf("Expected not to have authorization enabled")
}
}

0 comments on commit ca3be3c

Please sign in to comment.