package backend import ( "context" "crypto/rand" "encoding/base64" "errors" "time" ) type UserType string var ( UtPerson UserType = "person" UtHost UserType = "host" UtService UserType = "service" ErrTooManyAttempts error = errors.New("too many attempts") ErrReservationSniped error = errors.New("session ID reservation expired and was sniped") ErrBackendData error = errors.New("invalid data from backend") ) // Session holds info about the logged-in user that won't change type Session struct { SessionID string Expiration time.Time UserID string UserType UserType LdapDN string } // SessionCache holds volatile information about the logged-in user type SessionCache struct { Valid bool Groups []string DisplayName string GivenName string FamilyName string Email string } type Backend interface { PutSession(ctx context.Context, session Session, cache SessionCache) error GetSession(ctx context.Context, id string) (Session, *SessionCache, error) EndSession(ctx context.Context, id string) // ReserveSessionID attempts to reserve a session ID. If the ID was successfully reserved, return true // A reserved session ID should time out no sooner than 60s from the reservation. // Ergo, once reserved, a session ID should be used within 60s ReserveSessionID(ctx context.Context, sessionID string) (bool, error) DoMaintenance(ctx context.Context) Ping(ctx context.Context) error } func NewSessionID(ctx context.Context, backend Backend) (string, error) { var data = make([]byte, 18) for i := 0; i < 10; i++ { _, err := rand.Read(data[2:]) if err != nil { return "", err } encoded := base64.URLEncoding.EncodeToString(data) success, err := backend.ReserveSessionID(ctx, encoded) if err != nil { return "", err } if success { return encoded, nil } } return "", ErrTooManyAttempts }