-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request juju#6180 from axw/login-local-macaroon-part2
apiserver/authentication: introduce Interactions Add the Interactions type, which will manage time-limited local-user authentication interactions. This will be used in a following patch, which introduces support for discharging third-party macaroons from Juju itself. (Review request: http://reviews.vapour.ws/r/5618/)
- Loading branch information
Showing
5 changed files
with
334 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
// Copyright 2016 Canonical Ltd. All rights reserved. | ||
// Licensed under the AGPLv3, see LICENCE file for details. | ||
|
||
package authentication | ||
|
||
import ( | ||
"crypto/rand" | ||
"fmt" | ||
"sync" | ||
"time" | ||
|
||
"github.com/juju/errors" | ||
"gopkg.in/juju/names.v2" | ||
) | ||
|
||
// ErrWaitCanceled is returned by Interactions.Wait when the cancel | ||
// channel is signalled. | ||
var ErrWaitCanceled = errors.New("wait canceled") | ||
|
||
// ErrExpired is returned by Interactions.Wait when interactions expire | ||
// before they are done. | ||
var ErrExpired = errors.New("interaction timed out") | ||
|
||
// Interactions maintains a set of Interactions. | ||
type Interactions struct { | ||
mu sync.Mutex | ||
items map[string]*item | ||
} | ||
|
||
type item struct { | ||
c chan Interaction | ||
caveatId string | ||
expiry time.Time | ||
done bool | ||
} | ||
|
||
// Interaction records details of an in-progress interactive | ||
// macaroon-based login. | ||
type Interaction struct { | ||
CaveatId string | ||
LoginUser names.UserTag | ||
LoginError error | ||
} | ||
|
||
// NewInteractions returns a new Interactions. | ||
func NewInteractions() *Interactions { | ||
return &Interactions{ | ||
items: make(map[string]*item), | ||
} | ||
} | ||
|
||
func newId() (string, error) { | ||
var id [12]byte | ||
if _, err := rand.Read(id[:]); err != nil { | ||
return "", fmt.Errorf("cannot read random id: %v", err) | ||
} | ||
return fmt.Sprintf("%x", id[:]), nil | ||
} | ||
|
||
// Start records the start of an interactive login, and returns a random ID | ||
// that uniquely identifies it. A call to Wait with the same ID will return | ||
// the Interaction once it is done. | ||
func (m *Interactions) Start(caveatId string, expiry time.Time) (string, error) { | ||
id, err := newId() | ||
if err != nil { | ||
return "", err | ||
} | ||
m.mu.Lock() | ||
defer m.mu.Unlock() | ||
m.items[id] = &item{ | ||
c: make(chan Interaction, 1), | ||
caveatId: caveatId, | ||
expiry: expiry, | ||
} | ||
return id, nil | ||
} | ||
|
||
// Done signals that the user has either logged in, or attempted to and failed. | ||
func (m *Interactions) Done(id string, loginUser names.UserTag, loginError error) error { | ||
m.mu.Lock() | ||
defer m.mu.Unlock() | ||
item := m.items[id] | ||
|
||
if item == nil { | ||
return errors.NotFoundf("interaction %q", id) | ||
} | ||
if item.done { | ||
return errors.Errorf("interaction %q already done", id) | ||
} | ||
item.done = true | ||
item.c <- Interaction{ | ||
CaveatId: item.caveatId, | ||
LoginUser: loginUser, | ||
LoginError: loginError, | ||
} | ||
return nil | ||
} | ||
|
||
// Wait waits until the identified interaction is done, and returns the | ||
// corresponding Interaction. If the cancel channel is signalled before | ||
// the interaction is done, then ErrWaitCanceled is returned. If the | ||
// interaction expires before it is done, ErrExpired is returned. | ||
func (m *Interactions) Wait(id string, cancel <-chan struct{}) (*Interaction, error) { | ||
m.mu.Lock() | ||
item := m.items[id] | ||
m.mu.Unlock() | ||
if item == nil { | ||
return nil, errors.NotFoundf("interaction %q", id) | ||
} | ||
select { | ||
case <-cancel: | ||
return nil, ErrWaitCanceled | ||
case interaction, ok := <-item.c: | ||
if !ok { | ||
return nil, ErrExpired | ||
} | ||
m.mu.Lock() | ||
delete(m.items, id) | ||
m.mu.Unlock() | ||
return &interaction, nil | ||
} | ||
} | ||
|
||
// Expire removes any interactions that were due to expire by the | ||
// specified time. | ||
func (m *Interactions) Expire(t time.Time) { | ||
m.mu.Lock() | ||
defer m.mu.Unlock() | ||
for id, item := range m.items { | ||
if item.done || item.expiry.After(t) { | ||
continue | ||
} | ||
delete(m.items, id) | ||
close(item.c) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
// Copyright 2016 Canonical Ltd. All rights reserved. | ||
// Licensed under the AGPLv3, see LICENCE file for details. | ||
|
||
package authentication_test | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/juju/errors" | ||
"github.com/juju/testing" | ||
jc "github.com/juju/testing/checkers" | ||
gc "gopkg.in/check.v1" | ||
"gopkg.in/juju/names.v2" | ||
|
||
"github.com/juju/juju/apiserver/authentication" | ||
coretesting "github.com/juju/juju/testing" | ||
) | ||
|
||
type InteractionsSuite struct { | ||
testing.IsolationSuite | ||
interactions *authentication.Interactions | ||
} | ||
|
||
var _ = gc.Suite(&InteractionsSuite{}) | ||
|
||
func (s *InteractionsSuite) SetUpTest(c *gc.C) { | ||
s.IsolationSuite.SetUpTest(c) | ||
s.interactions = authentication.NewInteractions() | ||
} | ||
|
||
func (s *InteractionsSuite) TestStart(c *gc.C) { | ||
waitId, err := s.interactions.Start("caveat-id", time.Time{}) | ||
c.Assert(err, jc.ErrorIsNil) | ||
c.Assert(waitId, gc.Not(gc.Equals), "") | ||
} | ||
|
||
func (s *InteractionsSuite) TestDone(c *gc.C) { | ||
waitId := s.start(c, "caveat-id") | ||
err := s.interactions.Done(waitId, names.NewUserTag("admin@local"), nil) | ||
c.Assert(err, jc.ErrorIsNil) | ||
} | ||
|
||
func (s *InteractionsSuite) TestDoneNotFound(c *gc.C) { | ||
err := s.interactions.Done("not-found", names.NewUserTag("admin@local"), nil) | ||
c.Assert(err, jc.Satisfies, errors.IsNotFound) | ||
c.Assert(err, gc.ErrorMatches, `interaction "not-found" not found`) | ||
} | ||
|
||
func (s *InteractionsSuite) TestDoneTwice(c *gc.C) { | ||
waitId := s.start(c, "caveat-id") | ||
err := s.interactions.Done(waitId, names.NewUserTag("admin@local"), nil) | ||
c.Assert(err, jc.ErrorIsNil) | ||
err = s.interactions.Done(waitId, names.NewUserTag("admin@local"), nil) | ||
c.Assert(err, gc.ErrorMatches, `interaction ".*" already done`) | ||
} | ||
|
||
func (s *InteractionsSuite) TestWait(c *gc.C) { | ||
waitId := s.start(c, "caveat-id") | ||
loginUser := names.NewUserTag("admin@local") | ||
loginError := errors.New("login failed") | ||
s.done(c, waitId, loginUser, loginError) | ||
interaction, err := s.interactions.Wait(waitId, nil) | ||
c.Assert(err, jc.ErrorIsNil) | ||
c.Assert(interaction, gc.NotNil) | ||
c.Assert(interaction, jc.DeepEquals, &authentication.Interaction{ | ||
CaveatId: "caveat-id", | ||
LoginUser: loginUser, | ||
LoginError: loginError, | ||
}) | ||
} | ||
|
||
func (s *InteractionsSuite) TestWaitNotFound(c *gc.C) { | ||
interaction, err := s.interactions.Wait("not-found", nil) | ||
c.Assert(err, gc.ErrorMatches, `interaction "not-found" not found`) | ||
c.Assert(interaction, gc.IsNil) | ||
} | ||
|
||
func (s *InteractionsSuite) TestWaitTwice(c *gc.C) { | ||
waitId := s.start(c, "caveat-id") | ||
s.done(c, waitId, names.NewUserTag("admin@local"), nil) | ||
|
||
_, err := s.interactions.Wait(waitId, nil) | ||
c.Assert(err, jc.ErrorIsNil) | ||
|
||
// The Wait call above should have removed the item. | ||
_, err = s.interactions.Wait(waitId, nil) | ||
c.Assert(err, gc.ErrorMatches, `interaction ".*" not found`) | ||
} | ||
|
||
func (s *InteractionsSuite) TestWaitCancellation(c *gc.C) { | ||
waitId := s.start(c, "caveat-id") | ||
|
||
cancel := make(chan struct{}) | ||
waitResult := make(chan error) | ||
go func() { | ||
_, err := s.interactions.Wait(waitId, cancel) | ||
waitResult <- err | ||
}() | ||
|
||
// Wait should not pass until we've cancelled. | ||
select { | ||
case err := <-waitResult: | ||
c.Fatalf("unexpected result: %v", err) | ||
case <-time.After(coretesting.ShortWait): | ||
} | ||
|
||
cancel <- struct{}{} | ||
select { | ||
case err := <-waitResult: | ||
c.Assert(err, gc.Equals, authentication.ErrWaitCanceled) | ||
case <-time.After(coretesting.LongWait): | ||
c.Fatalf("timed out waiting for Wait to return") | ||
} | ||
} | ||
|
||
func (s *InteractionsSuite) TestWaitExpired(c *gc.C) { | ||
t0 := time.Now() | ||
t1 := t0.Add(time.Second) | ||
t2 := t1.Add(time.Second) | ||
|
||
waitId, err := s.interactions.Start("caveat-id", t2) | ||
c.Assert(err, jc.ErrorIsNil) | ||
|
||
type waitResult struct { | ||
interaction *authentication.Interaction | ||
err error | ||
} | ||
waitResultC := make(chan waitResult) | ||
go func() { | ||
interaction, err := s.interactions.Wait(waitId, nil) | ||
waitResultC <- waitResult{interaction, err} | ||
}() | ||
|
||
// This should do nothing, because there's nothing | ||
// due to expire until t2. | ||
s.interactions.Expire(t1) | ||
|
||
// Wait should not pass until the interaction expires. | ||
select { | ||
case result := <-waitResultC: | ||
c.Fatalf("unexpected result: %v", result) | ||
case <-time.After(coretesting.ShortWait): | ||
} | ||
|
||
s.interactions.Expire(t2) | ||
select { | ||
case result := <-waitResultC: | ||
c.Assert(result.err, gc.Equals, authentication.ErrExpired) | ||
c.Assert(result.interaction, gc.IsNil) | ||
case <-time.After(coretesting.LongWait): | ||
c.Fatalf("timed out waiting for Wait to return") | ||
} | ||
} | ||
|
||
func (s *InteractionsSuite) start(c *gc.C, caveatId string) string { | ||
waitId, err := s.interactions.Start(caveatId, time.Time{}) | ||
c.Assert(err, jc.ErrorIsNil) | ||
return waitId | ||
} | ||
|
||
func (s *InteractionsSuite) done(c *gc.C, waitId string, loginUser names.UserTag, loginError error) { | ||
err := s.interactions.Done(waitId, loginUser, loginError) | ||
c.Assert(err, jc.ErrorIsNil) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters