Skip to content

Commit

Permalink
Auth{n,z} refactor; fix token generation for 1.8
Browse files Browse the repository at this point in the history
Since 1.8 Docker will always request push and pull actions (scopes),
and we should return the subset that is allowed.
  • Loading branch information
rojer committed Aug 20, 2015
1 parent 79817b0 commit 5814407
Show file tree
Hide file tree
Showing 12 changed files with 280 additions and 163 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ See the [example config files](https://github.com/cesanta/docker_auth/tree/maste

Run with increased verbosity:
```{r, engine='bash', count_lines}
docker run ... cesanta/docker_auth --v=2 /config/auth_config.yml
docker run ... cesanta/docker_auth --v=2 --alsologtostderr /config/auth_config.yml
```

## Contributing
Expand Down
17 changes: 13 additions & 4 deletions auth_server/authn/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,28 @@

package authn

import "errors"

// Authentication plugin interface.
// Implementations must be goroutine-safe.
type Authenticator interface {
// Given a user name and a password (plain text), responds with nil on success
// or with any other error on failure.
Authenticate(user string, password PasswordString) error
// Given a user name and a password (plain text), responds with the result or an error.
// Error should only be reported if request could not be serviced, not if it should be denied.
// A special NoMatch error is returned if the authorizer could not reach a decision,
// e.g. none of the rules matched.
// Implementations must be goroutine-safe.
Authenticate(user string, password PasswordString) (bool, error)

// Finalize resources in preparation for shutdown.
// When this call is made there are guaranteed to be no Authenticate requests in flight
// and there will be no more calls made to this instance.
Stop()

// Human-readable name of the authenticator.
Name() string
}

var NoMatch = errors.New("did not match any rule")

//go:generate go-bindata -pkg authn -modtime 1 -mode 420 data/

type PasswordString string
Expand Down
5 changes: 2 additions & 3 deletions auth_server/authn/bindata.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 20 additions & 9 deletions auth_server/authn/google_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func (ga *GoogleAuth) doGoogleAuthCreateToken(rw http.ResponseWriter, code strin
}

if c2t.RefreshToken == "" {
http.Error(rw, "Server did not return refresh token. Log out and log in again.", http.StatusBadRequest)
http.Error(rw, "Google did not return refresh token, please sign out and sign in again.", http.StatusBadRequest)
return
}

Expand Down Expand Up @@ -292,6 +292,7 @@ func (ga *GoogleAuth) getIDTokenInfo(token string) (*GoogleTokenInfo, error) {
if err != nil {
return nil, err
}
glog.V(2).Infof("Token for %s, expires in %d", ti.Email, ti.ExpiresIn)
return &ti, nil
}

Expand Down Expand Up @@ -359,7 +360,7 @@ func (ga *GoogleAuth) getDBValue(user string) (*TokenDBValue, error) {
valueStr, err := ga.db.Get(getDBKey(user), nil)
switch {
case err == leveldb.ErrNotFound:
return nil, errors.New("no existing token, please sign in")
return nil, nil
case err != nil:
glog.Errorf("error accessing token db: %s", err)
return nil, fmt.Errorf("error accessing token db: %s", err)
Expand All @@ -375,7 +376,10 @@ func (ga *GoogleAuth) getDBValue(user string) (*TokenDBValue, error) {

func (ga *GoogleAuth) validateServerToken(user string) (*TokenDBValue, error) {
v, err := ga.getDBValue(user)
if err != nil {
if err != nil || v == nil {
if err == nil {
err = errors.New("no db value, please sign out and sign in again.")
}
return nil, err
}
if time.Now().After(v.ValidUntil) {
Expand Down Expand Up @@ -443,7 +447,7 @@ func (ga *GoogleAuth) doGoogleAuthCheck(rw http.ResponseWriter, token string) {
}
// Truncate to seconds for presentation.
texp := time.Duration(int64(dbv.ValidUntil.Sub(time.Now()).Seconds())) * time.Second
fmt.Fprintf(rw, `Server token for %s validated, expires in %s`, ti.Email, texp)
fmt.Fprintf(rw, "Server token for %s validated, expires in %s", ti.Email, texp)
}

func (ga *GoogleAuth) doGoogleAuthSignOut(rw http.ResponseWriter, token string) {
Expand All @@ -457,24 +461,31 @@ func (ga *GoogleAuth) doGoogleAuthSignOut(rw http.ResponseWriter, token string)
fmt.Fprint(rw, "signed out")
}

func (ga *GoogleAuth) Authenticate(user string, password PasswordString) error {
func (ga *GoogleAuth) Authenticate(user string, password PasswordString) (bool, error) {
dbv, err := ga.getDBValue(user)
if err != nil {
return err
return false, err
}
if dbv == nil {
return false, NoMatch
}
if time.Now().After(dbv.ValidUntil) {
dbv, err = ga.validateServerToken(user)
if err != nil {
return err
return false, err
}
}
if bcrypt.CompareHashAndPassword([]byte(dbv.DockerPassword), []byte(password)) != nil {
return errors.New("wrong password")
return false, nil
}
return nil
return true, nil
}

func (ga *GoogleAuth) Stop() {
ga.db.Close()
glog.Info("Token DB closed")
}

func (ga *GoogleAuth) Name() string {
return "Google"
}
13 changes: 8 additions & 5 deletions auth_server/authn/static_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package authn

import (
"encoding/json"
"errors"
"golang.org/x/crypto/bcrypt"
)

Expand All @@ -45,18 +44,22 @@ func NewStaticUserAuth(users map[string]*Requirements) *staticUsersAuth {
return &staticUsersAuth{users: users}
}

func (sua *staticUsersAuth) Authenticate(user string, password PasswordString) error {
func (sua *staticUsersAuth) Authenticate(user string, password PasswordString) (bool, error) {
reqs := sua.users[user]
if reqs == nil {
return errors.New("unknown user")
return false, NoMatch
}
if reqs.Password != nil {
if bcrypt.CompareHashAndPassword([]byte(*reqs.Password), []byte(password)) != nil {
return errors.New("wrong password")
return false, nil
}
}
return nil
return true, nil
}

func (sua *staticUsersAuth) Stop() {
}

func (sua *staticUsersAuth) Name() string {
return "static"
}
83 changes: 83 additions & 0 deletions auth_server/authz/acl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package authz

import (
"encoding/json"
"path"
"regexp"

"github.com/golang/glog"
)

type ACL []ACLEntry

type ACLEntry struct {
Match *MatchConditions `yaml:"match"`
Actions *[]string `yaml:"actions,flow"`
}

type MatchConditions struct {
Account *string `yaml:"account,omitempty" json:"account,omitempty"`
Type *string `yaml:"type,omitempty" json:"type,omitempty"`
Name *string `yaml:"name,omitempty" json:"name,omitempty"`
}

type aclAuthorizer struct {
acl ACL
}

func NewACLAuthorizer(acl ACL) Authorizer {
return &aclAuthorizer{acl: acl}
}

func (aa *aclAuthorizer) Authorize(ai *AuthRequestInfo) ([]string, error) {
for _, e := range aa.acl {
matched := e.Matches(ai)
if matched {
glog.V(2).Infof("%s matched %s", ai, e)
if len(*e.Actions) == 1 && (*e.Actions)[0] == "*" {
return ai.Actions, nil
}
return StringSetIntersection(ai.Actions, *e.Actions), nil
}
}
return nil, NoMatch
}

func (aa *aclAuthorizer) Stop() {
// Nothing to do.
}

func (aa *aclAuthorizer) Name() string {
return "static ACL"
}

type aclEntryJSON *ACLEntry

func (e ACLEntry) String() string {
b, _ := json.Marshal(e)
return string(b)
}

func matchString(pp *string, s string) bool {
if pp == nil {
return true
}
p := *pp
var matched bool
var err error
if len(p) > 2 && p[0] == '/' && p[len(p)-1] == '/' {
matched, err = regexp.Match(p[1:len(p)-1], []byte(s))
} else {
matched, err = path.Match(p, s)
}
return err == nil && matched
}

func (e *ACLEntry) Matches(ai *AuthRequestInfo) bool {
if matchString(e.Match.Account, ai.Account) &&
matchString(e.Match.Type, ai.Type) &&
matchString(e.Match.Name, ai.Name) {
return true
}
return false
}
44 changes: 44 additions & 0 deletions auth_server/authz/authz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package authz

import (
"errors"
"fmt"
"strings"
)

// Authorizer interface performs authorization of the request.
// It is invoked after authentication so it can be assumed that the requestor has
// presented satisfactory credentials for Account.
// Principally, it answers the question: is this Account allowed to perform these Actions
// on this Type.Name subject in the give Service?
type Authorizer interface {
// Authorize performs authorization given the request information.
// It returns a set of authorized actions (of the set requested), which can be empty/nil.
// Error should only be reported if request could not be serviced, not if it should be denied.
// A special NoMatch error is returned if the authorizer could not reach a decision,
// e.g. none of the rules matched.
// Implementations must be goroutine-safe.
Authorize(ai *AuthRequestInfo) ([]string, error)

// Finalize resources in preparation for shutdown.
// When this call is made there are guaranteed to be no Authenticate requests in flight
// and there will be no more calls made to this instance.
Stop()

// Human-readable name of the authenticator.
Name() string
}

var NoMatch = errors.New("did not match any rule")

type AuthRequestInfo struct {
Account string
Type string
Name string
Service string
Actions []string
}

func (ai AuthRequestInfo) String() string {
return fmt.Sprintf("{%s %s %s %s}", ai.Account, strings.Join(ai.Actions, ","), ai.Type, ai.Name)
}
26 changes: 26 additions & 0 deletions auth_server/authz/set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package authz

import (
"sort"

mapset "github.com/deckarep/golang-set"
)

func makeSet(ss []string) mapset.Set {
set := mapset.NewSet()
for _, s := range ss {
set.Add(s)
}
return set
}

func StringSetIntersection(a, b []string) []string {
as := makeSet(a)
bs := makeSet(b)
d := []string{}
for s := range as.Intersect(bs).Iter() {
d = append(d, s.(string))
}
sort.Strings(d)
return d
}
Loading

0 comments on commit 5814407

Please sign in to comment.