Skip to content

Commit

Permalink
add client diagnostics method
Browse files Browse the repository at this point in the history
add client diagnostics method
  • Loading branch information
jcmturner authored Feb 22, 2020
1 parent 4ba5599 commit f1c93a8
Show file tree
Hide file tree
Showing 13 changed files with 577 additions and 15 deletions.
7 changes: 7 additions & 0 deletions v8/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ REALM.COM = {
```
See https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html#realms for more information.

#### Client Diagnostics
In the event of issues the configuration of a client can be investigated with its ``Diagnostics`` method.
This will check that the required enctypes defined in the client's krb5 config are available in its keytab.
It will also check that KDCs can be resolved for the client's REALM.
The error returned will contain details of any failed checks.
The configuration details of the client will be written to the ``io.Writer`` provided.

---

### Kerberised Service
Expand Down
28 changes: 26 additions & 2 deletions v8/client/cache.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package client

import (
"encoding/json"
"errors"
"sort"
"sync"
"time"

Expand All @@ -17,12 +19,13 @@ type Cache struct {

// CacheEntry holds details for a cache entry.
type CacheEntry struct {
Ticket messages.Ticket
SPN string
Ticket messages.Ticket `json:"-"`
AuthTime time.Time
StartTime time.Time
EndTime time.Time
RenewTill time.Time
SessionKey types.EncryptionKey
SessionKey types.EncryptionKey `json:"-"`
}

// NewCache creates a new client ticket cache instance.
Expand All @@ -40,12 +43,33 @@ func (c *Cache) getEntry(spn string) (CacheEntry, bool) {
return e, ok
}

// JSON returns information about the cached service tickets in a JSON format.
func (c *Cache) JSON() (string, error) {
c.mux.RLock()
defer c.mux.RUnlock()
var es []CacheEntry
keys := make([]string, 0, len(c.Entries))
for k := range c.Entries {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
es = append(es, c.Entries[k])
}
b, err := json.MarshalIndent(&es, "", " ")
if err != nil {
return "", err
}
return string(b), nil
}

// addEntry adds a ticket to the cache.
func (c *Cache) addEntry(tkt messages.Ticket, authTime, startTime, endTime, renewTill time.Time, sessionKey types.EncryptionKey) CacheEntry {
spn := tkt.SName.PrincipalNameString()
c.mux.Lock()
defer c.mux.Unlock()
(*c).Entries[spn] = CacheEntry{
SPN: spn,
Ticket: tkt,
AuthTime: authTime,
StartTime: startTime,
Expand Down
146 changes: 146 additions & 0 deletions v8/client/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package client

import (
"fmt"
"sync"
"testing"
"time"

"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/types"
"github.com/stretchr/testify/assert"
)

func TestCache_addEntry_getEntry_remove_clear(t *testing.T) {
t.Parallel()
c := NewCache()
cnt := 10
var wg sync.WaitGroup
for i := 0; i < cnt; i++ {
wg.Add(1)
tkt := messages.Ticket{
SName: types.PrincipalName{
NameType: 1,
NameString: []string{fmt.Sprintf("%d", i), "test.cache"},
},
}
key := types.EncryptionKey{
KeyType: 1,
KeyValue: []byte{byte(i)},
}
go func(i int) {
e := c.addEntry(tkt, time.Unix(int64(0+i), 0).UTC(), time.Unix(int64(10+i), 0).UTC(), time.Unix(int64(20+i), 0).UTC(), time.Unix(int64(30+i), 0).UTC(), key)
assert.Equal(t, fmt.Sprintf("%d/test.cache", i), e.SPN, "SPN cache key not as expected")
wg.Done()
}(i)
}
wg.Wait()
for i := 0; i < cnt; i++ {
wg.Add(1)
go func(i int) {
e, ok := c.getEntry(fmt.Sprintf("%d/test.cache", i))
assert.True(t, ok, "cache entry %d was not found", i)
assert.Equal(t, time.Unix(int64(0+i), 0).UTC(), e.AuthTime, "auth time not as expected")
assert.Equal(t, time.Unix(int64(10+i), 0).UTC(), e.StartTime, "start time not as expected")
assert.Equal(t, time.Unix(int64(20+i), 0).UTC(), e.EndTime, "end time not as expected")
assert.Equal(t, time.Unix(int64(30+i), 0).UTC(), e.RenewTill, "renew time not as expected")
assert.Equal(t, []string{fmt.Sprintf("%d", i), "test.cache"}, e.Ticket.SName.NameString, "ticket not correct")
assert.Equal(t, []byte{byte(i)}, e.SessionKey.KeyValue, "session key not correct")
wg.Done()
}(i)
}
wg.Wait()
_, ok := c.getEntry(fmt.Sprintf("%d/test.cache", cnt+1))
assert.False(t, ok, "entry found in cache when it shouldn't have been")

// Remove just the even entries
for i := 0; i < cnt; i += 2 {
wg.Add(1)
go func(i int) {
c.RemoveEntry(fmt.Sprintf("%d/test.cache", i))
wg.Done()
}(i)
}
wg.Wait()

for i := 0; i < cnt; i++ {
wg.Add(1)
go func(i int) {
if i%2 == 0 {
_, ok := c.getEntry(fmt.Sprintf("%d/test.cache", cnt+1))
assert.False(t, ok, "entry %d found in cache when it shouldn't have been", i)
} else {
e, ok := c.getEntry(fmt.Sprintf("%d/test.cache", i))
assert.True(t, ok, "cache entry %d was not found", i)
assert.Equal(t, time.Unix(int64(0+i), 0).UTC(), e.AuthTime, "auth time not as expected")
assert.Equal(t, time.Unix(int64(10+i), 0).UTC(), e.StartTime, "start time not as expected")
assert.Equal(t, time.Unix(int64(20+i), 0).UTC(), e.EndTime, "end time not as expected")
assert.Equal(t, time.Unix(int64(30+i), 0).UTC(), e.RenewTill, "renew time not as expected")
assert.Equal(t, []string{fmt.Sprintf("%d", i), "test.cache"}, e.Ticket.SName.NameString, "ticket not correct")
assert.Equal(t, []byte{byte(i)}, e.SessionKey.KeyValue, "session key not correct")
}
wg.Done()
}(i)
}
wg.Wait()

// Clear the cache
c.clear()
for i := 0; i < cnt; i++ {
wg.Add(1)
go func(i int) {
_, ok := c.getEntry(fmt.Sprintf("%d/test.cache", cnt+1))
assert.False(t, ok, "entry %d found in cache when it shouldn't have been", i)
wg.Done()
}(i)
}
wg.Wait()
}

func TestCache_JSON(t *testing.T) {
t.Parallel()
c := NewCache()
cnt := 3
for i := 0; i < cnt; i++ {
tkt := messages.Ticket{
SName: types.PrincipalName{
NameType: 1,
NameString: []string{fmt.Sprintf("%d", i), "test.cache"},
},
}
key := types.EncryptionKey{
KeyType: 1,
KeyValue: []byte{byte(i)},
}
e := c.addEntry(tkt, time.Unix(int64(0+i), 0).UTC(), time.Unix(int64(10+i), 0).UTC(), time.Unix(int64(20+i), 0).UTC(), time.Unix(int64(30+i), 0).UTC(), key)
assert.Equal(t, fmt.Sprintf("%d/test.cache", i), e.SPN, "SPN cache key not as expected")
}
expected := `[
{
"SPN": "0/test.cache",
"AuthTime": "1970-01-01T00:00:00Z",
"StartTime": "1970-01-01T00:00:10Z",
"EndTime": "1970-01-01T00:00:20Z",
"RenewTill": "1970-01-01T00:00:30Z"
},
{
"SPN": "1/test.cache",
"AuthTime": "1970-01-01T00:00:01Z",
"StartTime": "1970-01-01T00:00:11Z",
"EndTime": "1970-01-01T00:00:21Z",
"RenewTill": "1970-01-01T00:00:31Z"
},
{
"SPN": "2/test.cache",
"AuthTime": "1970-01-01T00:00:02Z",
"StartTime": "1970-01-01T00:00:12Z",
"EndTime": "1970-01-01T00:00:22Z",
"RenewTill": "1970-01-01T00:00:32Z"
}
]`
j, err := c.JSON()
if err != nil {
t.Errorf("error getting json output of cache: %v", err)
}
assert.Equal(t, expected, j, "json output not as expected")
}
88 changes: 88 additions & 0 deletions v8/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
package client

import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"time"

"github.com/jcmturner/gokrb5/v8/config"
Expand Down Expand Up @@ -239,3 +242,88 @@ func (cl *Client) Destroy() {
cl.Credentials = creds
cl.Log("client destroyed")
}

// Diagnostics runs a set of checks that the client is properly configured and writes details to the io.Writer provided.
func (cl *Client) Diagnostics(w io.Writer) error {
cl.Print(w)
var errs []string
if cl.Credentials.HasKeytab() {
var loginRealmEncTypes []int32
for _, e := range cl.Credentials.Keytab().Entries {
if e.Principal.Realm == cl.Credentials.Realm() {
loginRealmEncTypes = append(loginRealmEncTypes, e.Key.KeyType)
}
}
for _, et := range cl.Config.LibDefaults.DefaultTktEnctypeIDs {
var etInKt bool
for _, val := range loginRealmEncTypes {
if val == et {
etInKt = true
break
}
}
if !etInKt {
errs = append(errs, fmt.Sprintf("default_tkt_enctypes specifies %d but this enctype is not available in the client's keytab", et))
}
}
for _, et := range cl.Config.LibDefaults.PreferredPreauthTypes {
var etInKt bool
for _, val := range loginRealmEncTypes {
if int(val) == et {
etInKt = true
break
}
}
if !etInKt {
errs = append(errs, fmt.Sprintf("preferred_preauth_types specifies %d but this enctype is not available in the client's keytab", et))
}
}
}
udpCnt, udpKDC, err := cl.Config.GetKDCs(cl.Credentials.Realm(), false)
if err != nil {
errs = append(errs, fmt.Sprintf("error when resolving KDCs for UDP communication: %v", err))
}
if udpCnt < 1 {
errs = append(errs, "no KDCs resolved for communication via UDP.")
} else {
b, _ := json.MarshalIndent(&udpKDC, "", " ")
fmt.Fprintf(w, "UDP KDCs: %s\n", string(b))
}
tcpCnt, tcpKDC, err := cl.Config.GetKDCs(cl.Credentials.Realm(), false)
if err != nil {
errs = append(errs, fmt.Sprintf("error when resolving KDCs for TCP communication: %v", err))
}
if tcpCnt < 1 {
errs = append(errs, "no KDCs resolved for communication via TCP.")
} else {
b, _ := json.MarshalIndent(&tcpKDC, "", " ")
fmt.Fprintf(w, "TCP KDCs: %s\n", string(b))
}

if errs == nil || len(errs) < 1 {
return nil
}
err = fmt.Errorf(strings.Join(errs, "\n"))
return err
}

// Print writes the details of the client to the io.Writer provided.
func (cl *Client) Print(w io.Writer) {
c, _ := cl.Credentials.JSON()
fmt.Fprintf(w, "Credentials:\n%s\n", c)

s, _ := cl.sessions.JSON()
fmt.Fprintf(w, "TGT Sessions:\n%s\n", s)

c, _ = cl.cache.JSON()
fmt.Fprintf(w, "Service ticket cache:\n%s\n", c)

s, _ = cl.settings.JSON()
fmt.Fprintf(w, "Settings:\n%s\n", s)

j, _ := cl.Config.JSON()
fmt.Fprintf(w, "Krb5 config:\n%s\n", j)

k, _ := cl.Credentials.Keytab().JSON()
fmt.Fprintf(w, "Keytab:\n%s\n", k)
}
3 changes: 1 addition & 2 deletions v8/client/client_ad_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/hex"
"log"
"testing"

"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/iana/etypeID"
Expand All @@ -13,8 +14,6 @@ import (
"github.com/jcmturner/gokrb5/v8/test/testdata"
"github.com/jcmturner/gokrb5/v8/types"
"github.com/stretchr/testify/assert"

"testing"
)

func TestClient_SuccessfulLogin_AD(t *testing.T) {
Expand Down
Loading

0 comments on commit f1c93a8

Please sign in to comment.