diff --git a/providers/kv/kv.go b/providers/kv/kv.go index 4cec3f1..4900a50 100644 --- a/providers/kv/kv.go +++ b/providers/kv/kv.go @@ -367,6 +367,9 @@ func (p *KvProvider) Store(operators []*ab.OperatorData) error { return err } for _, u := range a.UserDatas { + if u.Ephemeral { + continue + } if err := p.StoreUser(u); err != nil { return err } diff --git a/providers/nsc/nsc.go b/providers/nsc/nsc.go index 6b2af24..83489ce 100644 --- a/providers/nsc/nsc.go +++ b/providers/nsc/nsc.go @@ -262,6 +262,9 @@ func (a *NscProvider) Store(operators []*authb.OperatorData) error { } for _, u := range account.UserDatas { + if u.Ephemeral { + continue + } if u.Modified { if err := s.StoreRaw([]byte(u.Token)); err != nil { return err diff --git a/tests/users_test.go b/tests/users_test.go index f5e0739..f6829ab 100644 --- a/tests/users_test.go +++ b/tests/users_test.go @@ -1,6 +1,7 @@ package tests import ( + "github.com/nats-io/nkeys" "time" "github.com/nats-io/jwt/v2" @@ -478,3 +479,80 @@ func (t *ProviderSuite) Test_SetUserPermissionLimits() { t.True(u.BearerToken()) t.Contains(u.SubPermissions().Allow(), "hello") } + +func (t *ProviderSuite) Test_AddEphemeralUserWithIdentity() { + auth, err := authb.NewAuth(t.Provider) + t.NoError(err) + o, err := auth.Operators().Add("O") + t.NoError(err) + t.NotNil(o) + a, err := o.Accounts().Add("A") + t.NoError(err) + t.NotNil(a) + + uk, err := authb.KeyFor(nkeys.PrefixByteUser) + t.NoError(err) + + u, err := a.Users().AddWithIdentity("U", "", uk.Public) + t.NoError(err) + t.Equal(u.Subject(), uk.Public) + t.True(u.(*authb.UserData).Ephemeral) + + // should fail creds + _, err = u.Creds(time.Second) + t.Error(err) + + t.NoError(auth.Commit()) + t.NoError(auth.Reload()) + + o, err = auth.Operators().Get("O") + t.NoError(err) + a, err = o.Accounts().Get("A") + t.NoError(err) + _, err = a.Users().Get("U") + t.Error(err, authb.ErrNotFound) +} + +func (t *ProviderSuite) Test_AddWithIdentity() { + auth, err := authb.NewAuth(t.Provider) + t.NoError(err) + o, err := auth.Operators().Add("O") + t.NoError(err) + t.NotNil(o) + a, err := o.Accounts().Add("A") + t.NoError(err) + t.NotNil(a) + + uk, err := authb.KeyFor(nkeys.PrefixByteUser) + t.NoError(err) + + u, err := a.Users().AddWithIdentity("U", "", string(uk.Seed)) + t.NoError(err) + t.Equal(u.Subject(), uk.Public) + t.False(u.(*authb.UserData).Ephemeral) + + t.NoError(auth.Commit()) + t.NoError(auth.Reload()) + + o, err = auth.Operators().Get("O") + t.NoError(err) + a, err = o.Accounts().Get("A") + t.NoError(err) + u, err = a.Users().Get("U") + t.NoError(err) + t.NotNil(u) +} + +func (t *ProviderSuite) Test_AddWithIdentityRequiresUser() { + auth, err := authb.NewAuth(t.Provider) + t.NoError(err) + o, err := auth.Operators().Add("O") + t.NoError(err) + t.NotNil(o) + a, err := o.Accounts().Add("A") + t.NoError(err) + t.NotNil(a) + + _, err = a.Users().AddWithIdentity("U", "", "") + t.Error(err) +} diff --git a/types.go b/types.go index 9a5ff58..f890c98 100644 --- a/types.go +++ b/types.go @@ -178,6 +178,7 @@ type UserData struct { AccountData *AccountData RejectEdits bool Claim *jwt.UserClaims + Ephemeral bool } func (u *UserData) MarshalJSON() ([]byte, error) { @@ -335,11 +336,16 @@ type Account interface { // Users is an interface for managing users type Users interface { // Add creates a new User with the specified name and signed using - // the specified key. Note that you simply specify the public key + // the specified signer key. Note that you simply specify the public key // you want to use for signing, and the key must be one of the account's // signing keys. If the key is associated with a scope, the user will // be a scoped user. - Add(name string, key string) (User, error) + Add(name string, signer string) (User, error) + // AddWithIdentity creates user with the specified name and signed using + // the specified signer key. + // If the provided ID is only a public key the user will be ephemeral and will not stored, + // other operations, as cred generation will fail + AddWithIdentity(name string, signer string, id string) (User, error) // Delete the user by matching its name or subject Delete(name string) error // Get returns the user by matching its name or subject diff --git a/users.go b/users.go index 46e3a8d..9df7703 100644 --- a/users.go +++ b/users.go @@ -10,6 +10,14 @@ type UsersImpl struct { } func (a *UsersImpl) Add(name string, key string) (User, error) { + uk, err := a.accountData.Operator.SigningService.NewKey(nkeys.PrefixByteUser) + if err != nil { + return nil, err + } + return a.add(name, key, uk) +} + +func (a *UsersImpl) add(name string, key string, uk *Key) (User, error) { if key == "" { key = a.accountData.Key.Public } @@ -18,15 +26,13 @@ func (a *UsersImpl) Add(name string, key string) (User, error) { return nil, err } _, scoped := a.accountData.Claim.SigningKeys.GetScope(key) - uk, err := a.accountData.Operator.SigningService.NewKey(nkeys.PrefixByteUser) - if err != nil { - return nil, err - } + d := &UserData{ BaseData: BaseData{EntityName: name, Key: uk, Modified: true}, AccountData: a.accountData, Claim: jwt.NewUserClaims(uk.Public), RejectEdits: scoped, + Ephemeral: uk.Seed == nil, } d.Claim.Name = name if signingKey { @@ -41,10 +47,20 @@ func (a *UsersImpl) Add(name string, key string) (User, error) { return nil, err } a.accountData.UserDatas = append(a.accountData.UserDatas, d) - a.accountData.Operator.AddedKeys = append(a.accountData.Operator.AddedKeys, uk) + if !d.Ephemeral { + a.accountData.Operator.AddedKeys = append(a.accountData.Operator.AddedKeys, uk) + } return d, nil } +func (a *UsersImpl) AddWithIdentity(name string, key string, id string) (User, error) { + uk, err := KeyFrom(id, nkeys.PrefixByteUser) + if err != nil { + return nil, err + } + return a.add(name, key, uk) +} + func (a *UsersImpl) Get(name string) (User, error) { for _, u := range a.accountData.UserDatas { if u.EntityName == name || u.Claim.Subject == name {