Skip to content

Commit

Permalink
Merge pull request cesanta#55 from carsonoid/mongo_authn
Browse files Browse the repository at this point in the history
Mongo authn
  • Loading branch information
rojer committed Dec 16, 2015
2 parents 4fde76f + fcfc5c9 commit 5292646
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 76 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Supported authentication methods:
* Static list of users
* Google Sign-In (incl. Google for Work / GApps for domain) (documented [here](https://github.com/cesanta/docker_auth/blob/master/examples/reference.yml))
* LDAP bind
* MongoDB user collection

Supported authorization methods:
* Static ACL
Expand Down
2 changes: 1 addition & 1 deletion auth_server/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ all: build
local: build-local

update-deps:
go get -v -u -f github.com/tools/godep github.com/jteeuwen/go-bindata/... .
go get -v -u -f github.com/tools/godep github.com/jteeuwen/go-bindata/...

godep:
godep save
Expand Down
114 changes: 114 additions & 0 deletions auth_server/authn/mongo_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
Copyright 2015 Cesanta Software Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package authn

import (
"fmt"

"github.com/cesanta/docker_auth/auth_server/mgo_session"
"github.com/golang/glog"
"golang.org/x/crypto/bcrypt"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)

type MongoAuthConfig struct {
MongoConfig *mgo_session.Config `yaml:"dial_info,omitempty"`
Collection string `yaml:"collection,omitempty"`
}

type MongoAuth struct {
config *MongoAuthConfig
session *mgo.Session
Collection string `yaml:"collection,omitempty"`
}

type authUserEntry struct {
Username *string `yaml:"username,omitempty" json:"username,omitempty"`
Password *string `yaml:"password,omitempty" json:"password,omitempty"`
}

func NewMongoAuth(c *MongoAuthConfig) (*MongoAuth, error) {
// Attempt to create new mongo session.
session, err := mgo_session.New(c.MongoConfig)
if err != nil {
return nil, err
}

return &MongoAuth{
config: c,
session: session,
}, nil
}

func (mauth *MongoAuth) Authenticate(account string, password PasswordString) (bool, error) {
// Copy our session
tmp_session := mauth.session.Copy()
// Close up when we are done
defer tmp_session.Close()

// Get Users from MongoDB
glog.V(2).Infof("Checking user %s against Mongo Users. DB: %s, collection:%s",
account, mauth.config.MongoConfig.DialInfo.Database, mauth.config.Collection)
var dbUserRecord authUserEntry
collection := tmp_session.DB(mauth.config.MongoConfig.DialInfo.Database).C(mauth.config.Collection)
err := collection.Find(bson.M{"username": account}).One(&dbUserRecord)

// If we connect and get no results we return a NoMatch so auth can fall-through
if err == mgo.ErrNotFound {
return false, NoMatch
} else if err != nil {
return false, err
}

// Validate db password against passed password
if dbUserRecord.Password != nil {
if bcrypt.CompareHashAndPassword([]byte(*dbUserRecord.Password), []byte(password)) != nil {
return false, nil
}
}

// Auth success
return true, nil
}

// Validate ensures that any custom config options
// in a Config are set correctly.
func (c *MongoAuthConfig) Validate(configKey string) error {
//First validate the mongo config.
if err := c.MongoConfig.Validate(configKey); err != nil {
return err
}

// Now check additional config fields.
if c.Collection == "" {
return fmt.Errorf("%s.collection is required", configKey)
}

return nil
}

func (ma *MongoAuth) Stop() {
// Close connection to MongoDB database (if any)
if ma.session != nil {
ma.session.Close()
}
}

func (ga *MongoAuth) Name() string {
return "MongoDB"
}
90 changes: 37 additions & 53 deletions auth_server/authz/acl_mongo.go
Original file line number Diff line number Diff line change
@@ -1,79 +1,37 @@
package authz

import (
"errors"
"fmt"
"io/ioutil"
"strings"
"sync"
"time"

"github.com/cesanta/docker_auth/auth_server/mgo_session"
"github.com/golang/glog"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)

// ACLMongoConfig stores how to connect to the MongoDB server and how long
// an ACL remains valid until new ones will be fetched.
type ACLMongoConfig struct {
DialInfo ACLMongoDialConfig `yaml:"dial_info,omitempty"`
Collection string `yaml:"collection,omitempty"`
CacheTTL time.Duration `yaml:"cache_ttl,omitempty"`
}

// ACLMongoDialConfig stores how we connect to the MongoDB server
type ACLMongoDialConfig struct {
mgo.DialInfo `yaml:",inline"`
PasswordFile string `yaml:"password_file,omitempty"`
}

// Validate ensures the most common fields inside the mgo.DialInfo portion of
// an ACLMongoDialInfo are set correctly as well as other fields inside the
// ACLMongoConfig itself.
func (c *ACLMongoConfig) Validate() error {
if len(c.DialInfo.DialInfo.Addrs) == 0 {
return errors.New("At least one element in acl_mongo.dial_info.addrs is required")
}
if c.DialInfo.DialInfo.Timeout == 0 {
c.DialInfo.DialInfo.Timeout = 10 * time.Second
}
if c.DialInfo.DialInfo.Database == "" {
return errors.New("acl_mongo.dial_info.database is required")
}
if c.Collection == "" {
return errors.New("acl_mongo.collection is required")
}
if c.CacheTTL < 0 {
return errors.New(`acl_mongo.cache_ttl is required (e.g. "1m" for 1 minute)`)
}
return nil
MongoConfig *mgo_session.Config `yaml:"dial_info,omitempty"`
Collection string `yaml:"collection,omitempty"`
CacheTTL time.Duration `yaml:"cache_ttl,omitempty"`
}

type aclMongoAuthorizer struct {
lastCacheUpdate time.Time
lock sync.RWMutex
config ACLMongoConfig
config *ACLMongoConfig
staticAuthorizer Authorizer
session *mgo.Session
updateTicker *time.Ticker
Collection string `yaml:"collection,omitempty"`
CacheTTL time.Duration `yaml:"cache_ttl,omitempty"`
}

// NewACLMongoAuthorizer creates a new ACL Mongo authorizer
func NewACLMongoAuthorizer(c ACLMongoConfig) (Authorizer, error) {
// Attempt to create a MongoDB session which we can re-use when handling
// multiple auth requests.

// Read in the password (if any)
if c.DialInfo.PasswordFile != "" {
passBuf, err := ioutil.ReadFile(c.DialInfo.PasswordFile)
if err != nil {
return nil, fmt.Errorf(`Failed to read password file "%s": %s`, c.DialInfo.PasswordFile, err)
}
c.DialInfo.DialInfo.Password = strings.TrimSpace(string(passBuf))
}

glog.V(2).Infof("Creating MongoDB session (operation timeout %s)", c.DialInfo.DialInfo.Timeout)
session, err := mgo.DialWithInfo(&c.DialInfo.DialInfo)
func NewACLMongoAuthorizer(c *ACLMongoConfig) (Authorizer, error) {
// Attempt to create new mongo session.
session, err := mgo_session.New(c.MongoConfig)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -106,6 +64,25 @@ func (ma *aclMongoAuthorizer) Authorize(ai *AuthRequestInfo) ([]string, error) {
return ma.staticAuthorizer.Authorize(ai)
}

// Validate ensures that any custom config options
// in a Config are set correctly.
func (c *ACLMongoConfig) Validate(configKey string) error {
//First validate the mongo config.
if err := c.MongoConfig.Validate(configKey); err != nil {
return err
}

// Now check additional config fields.
if c.Collection == "" {
return fmt.Errorf("%s.collection is required", configKey)
}
if c.CacheTTL < 0 {
return fmt.Errorf("%s.cache_ttl is required (e.g. \"1m\" for 1 minute)", configKey)
}

return nil
}

func (ma *aclMongoAuthorizer) Stop() {
// This causes the background go routine which updates the ACL to stop
ma.updateTicker.Stop()
Expand Down Expand Up @@ -143,7 +120,14 @@ func (ma *aclMongoAuthorizer) continuouslyUpdateACLCache() {
func (ma *aclMongoAuthorizer) updateACLCache() error {
// Get ACL from MongoDB
var newACL ACL
collection := ma.session.DB(ma.config.DialInfo.DialInfo.Database).C(ma.config.Collection)

// Copy our session
tmp_session := ma.session.Copy()

// Close up when we are done
defer tmp_session.Close()

collection := tmp_session.DB(ma.config.MongoConfig.DialInfo.Database).C(ma.config.Collection)
err := collection.Find(bson.M{}).All(&newACL)
if err != nil {
return err
Expand Down
72 changes: 72 additions & 0 deletions auth_server/mgo_session/mgo_session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
Copyright 2015 Cesanta Software Ltmc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or impliemc.
See the License for the specific language governing permissions and
limitations under the License.
*/

package mgo_session

import (
"fmt"
"io/ioutil"
"strings"
"time"

"github.com/golang/glog"
"gopkg.in/mgo.v2"
)

// Config stores how to connect to the MongoDB server and an optional password file
type Config struct {
DialInfo mgo.DialInfo `yaml:",inline"`
PasswordFile string `yaml:"password_file,omitempty"`
}

// Validate ensures the most common fields inside the mgo.DialInfo portion of
// a Config are set correctly as well as other fields inside the
// Config itself.
func (c *Config) Validate(configKey string) error {
if len(c.DialInfo.Addrs) == 0 {
return fmt.Errorf("At least one element in %s.dial_info.addrs is required", configKey)
}
if c.DialInfo.Timeout == 0 {
c.DialInfo.Timeout = 10 * time.Second
}
if c.DialInfo.Database == "" {
return fmt.Errorf("%s.dial_info.database is required", configKey)
}
return nil
}

func New(c *Config) (*mgo.Session, error) {
// Attempt to create a MongoDB session which we can re-use when handling
// multiple requests. We can optionally read in the password from a file or directly from the config.

// Read in the password (if any)
if c.PasswordFile != "" {
passBuf, err := ioutil.ReadFile(c.PasswordFile)
if err != nil {
return nil, fmt.Errorf(`Failed to read password file "%s": %s`, c.PasswordFile, err)
}
c.DialInfo.Password = strings.TrimSpace(string(passBuf))
}

glog.V(2).Infof("Creating MongoDB session (operation timeout %s)", c.DialInfo.Timeout)

session, err := mgo.DialWithInfo(&c.DialInfo)
if err != nil {
return nil, err
}

return session, nil
}
28 changes: 17 additions & 11 deletions auth_server/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,14 @@ import (
)

type Config struct {
Server ServerConfig `yaml:"server"`
Token TokenConfig `yaml:"token"`
Users map[string]*authn.Requirements `yaml:"users,omitempty"`
GoogleAuth *authn.GoogleAuthConfig `yaml:"google_auth,omitempty"`
LDAPAuth *authn.LDAPAuthConfig `yaml:"ldap_auth,omitempty"`
ACL authz.ACL `yaml:"acl"`
ACLMongoConf *authz.ACLMongoConfig `yaml:"acl_mongo"`
Server ServerConfig `yaml:"server"`
Token TokenConfig `yaml:"token"`
Users map[string]*authn.Requirements `yaml:"users,omitempty"`
GoogleAuth *authn.GoogleAuthConfig `yaml:"google_auth,omitempty"`
LDAPAuth *authn.LDAPAuthConfig `yaml:"ldap_auth,omitempty"`
MongoAuth *authn.MongoAuthConfig `yaml:"mongo_auth,omitempty"`
ACL authz.ACL `yaml:"acl"`
ACLMongo *authz.ACLMongoConfig `yaml:"acl_mongo"`
}

type ServerConfig struct {
Expand Down Expand Up @@ -70,9 +71,14 @@ func validate(c *Config) error {
if c.Token.Expiration <= 0 {
return fmt.Errorf("expiration must be positive, got %d", c.Token.Expiration)
}
if c.Users == nil && c.GoogleAuth == nil && c.LDAPAuth == nil {
if c.Users == nil && c.GoogleAuth == nil && c.LDAPAuth == nil && c.MongoAuth == nil {
return errors.New("no auth methods are configured, this is probably a mistake. Use an empty user map if you really want to deny everyone.")
}
if c.MongoAuth != nil {
if err := c.MongoAuth.Validate("mongo_auth"); err != nil {
return err
}
}
if gac := c.GoogleAuth; gac != nil {
if gac.ClientSecretFile != "" {
contents, err := ioutil.ReadFile(gac.ClientSecretFile)
Expand All @@ -88,11 +94,11 @@ func validate(c *Config) error {
gac.HTTPTimeout = 10
}
}
if c.ACL == nil && c.ACLMongoConf == nil {
if c.ACL == nil && c.ACLMongo == nil {
return errors.New("ACL is empty, this is probably a mistake. Use an empty list if you really want to deny all actions")
}
if c.ACLMongoConf != nil {
if err := c.ACLMongoConf.Validate(); err != nil {
if c.ACLMongo != nil {
if err := c.ACLMongo.Validate("acl_mongo"); err != nil {
return err
}
}
Expand Down
Loading

0 comments on commit 5292646

Please sign in to comment.