From 40b8191362f725283d1ab3e875bc2699026aee6d Mon Sep 17 00:00:00 2001
From: Konrad Kleine
Date: Wed, 4 Nov 2015 14:30:14 +0100
Subject: [PATCH 001/209] Add comment field to ACLs
When ACLs are stored outside of the static YAML configuration file one
might be interested in a context about the ACL.
I proposed a database (e.g. MongoDB) ACL backend in #38 and that's why I
need the comment field on an ACL.
Signed-off-by: Konrad Kleine
---
auth_server/authz/acl.go | 3 ++-
examples/reference.yml | 11 ++++++-----
examples/simple.yml | 4 ++--
3 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/auth_server/authz/acl.go b/auth_server/authz/acl.go
index b0576d32..5c2313db 100644
--- a/auth_server/authz/acl.go
+++ b/auth_server/authz/acl.go
@@ -14,6 +14,7 @@ type ACL []ACLEntry
type ACLEntry struct {
Match *MatchConditions `yaml:"match"`
Actions *[]string `yaml:"actions,flow"`
+ Comment *string `yaml:"comment,omitempty"`
}
type MatchConditions struct {
@@ -34,7 +35,7 @@ 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)
+ glog.V(2).Infof("%s matched %s (Comment: %s)", ai, e, e.Comment)
if len(*e.Actions) == 1 && (*e.Actions)[0] == "*" {
return ai.Actions, nil
}
diff --git a/examples/reference.yml b/examples/reference.yml
index 6a70acc9..7d07efde 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -96,21 +96,22 @@ ldap_auth:
# * ${type} - the type of the entity, normally "repository".
# * ${name} - the name of the repository (i.e. image), e.g. centos.
acl:
- # Admin has full access to everything.
- match: {account: "admin"}
actions: ["*"]
- # User "test" has full access to test-* images but nothing else.
+ comment: "Admin has full access to everything."
- match: {account: "test", name: "test-*"}
actions: ["*"]
+ comment: "User \"test\" has full access to test-* images but nothing else. (1)"
- match: {account: "test"}
actions: []
- # All logged in users can pull all images.
+ comment: "User \"test\" has full access to test-* images but nothing else. (2)"
- match: {account: "/.+/"}
actions: ["pull"]
- # All logged in users can push all images that are in a namespace beginning with their name
+ comment: "All logged in users can pull all images."
- match: {account: "/.+/", name: "${account}/*"}
actions: ["*"]
- # Anonymous users can pull "hello-world".
+ comment: "All logged in users can push all images that are in a namespace beginning with their name"
- match: {account: "", name: "hello-world"}
actions: ["pull"]
+ comment: "Anonymous users can pull \"hello-world\"."
# Access is denied by default.
diff --git a/examples/simple.yml b/examples/simple.yml
index 820c4e7e..7ca3d6e1 100644
--- a/examples/simple.yml
+++ b/examples/simple.yml
@@ -24,10 +24,10 @@ users:
password: "$2y$05$WuwBasGDAgr.QCbGIjKJaep4dhxeai9gNZdmBnQXqpKly57oNutya" # 123
acl:
- # Admin has full access to everything.
- match: {account: "admin"}
actions: ["*"]
- # User "user" can pull stuff.
+ comment: "Admin has full access to everything."
- match: {account: "user"}
actions: ["pull"]
+ comment: "User \"user\" can pull stuff."
# Access is denied by default.
From 84329da78dbe824eed0aa6a0e9fce3bfbefa7816 Mon Sep 17 00:00:00 2001
From: Konrad Kleine
Date: Thu, 5 Nov 2015 10:46:07 +0100
Subject: [PATCH 002/209] Add MongoDB backend for ACLs
This adds the ability to store ACLs in a MongoDB database. There's also
a documentation file in docs/ACL_Backend_MongoDB.md that explains the
document layout inside MongoDB.
See also #38 for the initial discussion on this topic.
* There was an unreachable call to `w.Close()` which I've moved right
behind the error checks after the resource was opened as `defer
w.Close()`.
* When the server starts it no longer prints something like
`Config from config.yml (3 users, 0 ACL entries)` but `Config from
config.yml (3 users, 0 ACL static entries)`. Notice the word
**static** to denote the fact that there maybe an ACL backend for which
no ACL has been fetched yet.
* Made mgo.DialInfo and embedded struct in ACLMongoDialInfo
* ACL cache is secured with a sync.RWMutex and allows multiple
non-blocking reads.
* ACL are updated in a background go routine which is controlled by an
update ticker (time.Ticker). On startup and on every tick, the ACL
will be updated.
Signed-off-by: Konrad Kleine
---
auth_server/authz/acl.go | 6 +-
auth_server/authz/acl_mongo.go | 166 +++++++++++++++++++++++++++++++++
auth_server/main.go | 4 +-
auth_server/server/config.go | 22 +++--
auth_server/server/server.go | 16 +++-
docs/ACL_Backend_MongoDB.md | 73 +++++++++++++++
examples/reference.yml | 27 ++++++
7 files changed, 301 insertions(+), 13 deletions(-)
create mode 100644 auth_server/authz/acl_mongo.go
create mode 100644 docs/ACL_Backend_MongoDB.md
diff --git a/auth_server/authz/acl.go b/auth_server/authz/acl.go
index 5c2313db..04f2fa8d 100644
--- a/auth_server/authz/acl.go
+++ b/auth_server/authz/acl.go
@@ -27,8 +27,10 @@ type aclAuthorizer struct {
acl ACL
}
-func NewACLAuthorizer(acl ACL) Authorizer {
- return &aclAuthorizer{acl: acl}
+// NewACLAuthorizer Creates a new static authorizer with ACL that have been read from the config file
+func NewACLAuthorizer(acl ACL) (Authorizer, error) {
+ glog.V(1).Infof("Created ACL Authorizer with %d entries", len(acl))
+ return &aclAuthorizer{acl: acl}, nil
}
func (aa *aclAuthorizer) Authorize(ai *AuthRequestInfo) ([]string, error) {
diff --git a/auth_server/authz/acl_mongo.go b/auth_server/authz/acl_mongo.go
new file mode 100644
index 00000000..e722bdc3
--- /dev/null
+++ b/auth_server/authz/acl_mongo.go
@@ -0,0 +1,166 @@
+package authz
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "strings"
+ "sync"
+ "time"
+
+ "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
+}
+
+type aclMongoAuthorizer struct {
+ lastCacheUpdate time.Time
+ lock sync.RWMutex
+ config ACLMongoConfig
+ staticAuthorizer Authorizer
+ session *mgo.Session
+ updateTicker *time.Ticker
+}
+
+// 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)
+ if err != nil {
+ return nil, err
+ }
+
+ authorizer := &aclMongoAuthorizer{
+ config: c,
+ session: session,
+ updateTicker: time.NewTicker(c.CacheTTL),
+ }
+
+ // Initially fetch the ACL from MongoDB
+ if err := authorizer.updateACLCache(); err != nil {
+ return nil, err
+ }
+
+ go authorizer.continuouslyUpdateACLCache()
+
+ return authorizer, nil
+}
+
+func (ma *aclMongoAuthorizer) Authorize(ai *AuthRequestInfo) ([]string, error) {
+ ma.lock.RLock()
+ defer ma.lock.RUnlock()
+
+ // Test if authorizer has been initialized
+ if ma.staticAuthorizer == nil {
+ return nil, fmt.Errorf("MongoDB authorizer is not ready")
+ }
+
+ return ma.staticAuthorizer.Authorize(ai)
+}
+
+func (ma *aclMongoAuthorizer) Stop() {
+ // This causes the background go routine which updates the ACL to stop
+ ma.updateTicker.Stop()
+
+ // Close connection to MongoDB database (if any)
+ if ma.session != nil {
+ ma.session.Close()
+ }
+}
+
+func (ma *aclMongoAuthorizer) Name() string {
+ return "MongoDB ACL"
+}
+
+// continuouslyUpdateACLCache checks if the ACL cache has expired and depending
+// on the the result it updates the cache with the ACL from the MongoDB server.
+// The ACL will be stored inside the static authorizer instance which we use
+// to minimize duplication of code and maximize reuse of existing code.
+func (ma *aclMongoAuthorizer) continuouslyUpdateACLCache() {
+ var tick time.Time
+ for ; true; tick = <-ma.updateTicker.C {
+ aclAge := time.Now().Sub(ma.lastCacheUpdate)
+ glog.V(2).Infof("Updating ACL at %s (ACL age: %s. CacheTTL: %s)", tick, aclAge, ma.config.CacheTTL)
+
+ err := ma.updateACLCache()
+ if err == nil {
+ continue
+ }
+
+ glog.Errorf("Failed to update ACL. ERROR: %s", err)
+ glog.Warningf("Using stale ACL (Age: %s, TTL: %s)", aclAge, ma.config.CacheTTL)
+ }
+}
+
+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)
+ err := collection.Find(bson.M{}).All(&newACL)
+ if err != nil {
+ return err
+ }
+ glog.V(2).Infof("Number of new ACL entries from MongoDB: %d", len(newACL))
+
+ newStaticAuthorizer, err := NewACLAuthorizer(newACL)
+ if err != nil {
+ return err
+ }
+
+ ma.lock.Lock()
+ ma.lastCacheUpdate = time.Now()
+ ma.staticAuthorizer = newStaticAuthorizer
+ ma.lock.Unlock()
+
+ glog.V(2).Infof("Got new ACL from MongoDB: %s", newACL)
+ glog.V(1).Infof("Installed new ACL from MongoDB (%d entries)", len(newACL))
+ return nil
+}
diff --git a/auth_server/main.go b/auth_server/main.go
index 34c6bab0..4916c79b 100644
--- a/auth_server/main.go
+++ b/auth_server/main.go
@@ -40,7 +40,7 @@ type RestartableServer struct {
}
func ServeOnce(c *server.Config, cf string, hd *httpdown.HTTP) (*server.AuthServer, httpdown.Server) {
- glog.Infof("Config from %s (%d users, %d ACL entries)", cf, len(c.Users), len(c.ACL))
+ glog.Infof("Config from %s (%d users, %d ACL static entries)", cf, len(c.Users), len(c.ACL))
as, err := server.NewAuthServer(c)
if err != nil {
glog.Exitf("Failed to create auth server: %s", err)
@@ -101,6 +101,7 @@ func (rs *RestartableServer) WatchConfig() {
if err != nil {
glog.Fatalf("Failed to create watcher: %s", err)
}
+ defer w.Close()
stopSignals := make(chan os.Signal, 1)
signal.Notify(stopSignals, syscall.SIGTERM, syscall.SIGINT)
@@ -137,7 +138,6 @@ func (rs *RestartableServer) WatchConfig() {
glog.Exitf("Exiting")
}
}
- w.Close()
}
func (rs *RestartableServer) MaybeRestart() {
diff --git a/auth_server/server/config.go b/auth_server/server/config.go
index aa4fae61..d9575390 100644
--- a/auth_server/server/config.go
+++ b/auth_server/server/config.go
@@ -31,12 +31,13 @@ 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"`
+ 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"`
}
type ServerConfig struct {
@@ -87,8 +88,13 @@ func validate(c *Config) error {
gac.HTTPTimeout = 10
}
}
- if c.ACL == 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.ACL == nil && c.ACLMongoConf == 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 {
+ return err
+ }
}
return nil
}
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 54a6fa3b..7ea4873b 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -53,7 +53,21 @@ type AuthServer struct {
func NewAuthServer(c *Config) (*AuthServer, error) {
as := &AuthServer{
config: c,
- authorizers: []authz.Authorizer{authz.NewACLAuthorizer(c.ACL)},
+ authorizers: []authz.Authorizer{},
+ }
+ if c.ACL != nil {
+ staticAuthorizer, err := authz.NewACLAuthorizer(c.ACL)
+ if err != nil {
+ return nil, err
+ }
+ as.authorizers = append(as.authorizers, staticAuthorizer)
+ }
+ if c.ACLMongoConf != nil {
+ mongoAuthorizer, err := authz.NewACLMongoAuthorizer(*c.ACLMongoConf)
+ if err != nil {
+ return nil, err
+ }
+ as.authorizers = append(as.authorizers, mongoAuthorizer)
}
if c.Users != nil {
as.authenticators = append(as.authenticators, authn.NewStaticUserAuth(c.Users))
diff --git a/docs/ACL_Backend_MongoDB.md b/docs/ACL_Backend_MongoDB.md
new file mode 100644
index 00000000..281636eb
--- /dev/null
+++ b/docs/ACL_Backend_MongoDB.md
@@ -0,0 +1,73 @@
+# ACL backend in MongoDB
+
+Maybe you want to manage your ACL from an external application and therefore
+need them to be stored outside of your auth_server's configuration file.
+
+For this purpose, there's a [MongoDB](https://www.mongodb.org/) ACL backend
+which can query an ACL from a MongoDB database.
+
+A typical ACL entry from the static YAML configuration file looks something like
+this:
+
+```
+- match: {account: "/.+/", name: "${account}/*"}
+ actions: ["push", "pull"]
+ comment: "All logged in users can push all images that are in a namespace beginning with their name"
+```
+
+Notice the use of a regular expression (`/.+/`), a placeholder (`${account}`),
+and in particular the `actions` array.
+
+The ACL entry as is it is stored inside the static YAML file can be mapped to
+MongoDB quite easily. Below you can find a list of ACL entries that are ready to
+be imported into MongoDB. Those ACL entries reflect what's specified in the
+`example/reference.yml` file under the `acl` section (aka static ACL).
+
+**reference_acl.json**
+
+```json
+{"match" : {"account" : "admin"}, "actions" : ["*"], "comment" : "Admin has full access to everything."}
+{"match" : {"account" : "test", "name" : "test-*"}, "actions" : ["*"], "comment" : "User \"test\" has full access to test-* images but nothing else. (1)"}
+{"match" : {"account" : "test"}, "actions" : [], "comment" : "User \"test\" has full access to test-* images but nothing else. (2)"}
+{"match" : {"account" : "/.+/"}, "actions" : ["pull"], "comment" : "All logged in users can pull all images."}
+{"match" : {"account" : "/.+/", "name" : "${account}/*"}, "actions" : ["*"], "comment" : "All logged in users can push all images that are in a namespace beginning with their name"}
+{"match" : {"account" : "", "name" : "hello-world"}, "actions" : ["pull"], "comment" : "Anonymous users can pull \"hello-world\"."}
+```
+
+**Note** that each document entry must span exactly one line or otherwise the
+`mongoimport` tool (see below) will not accept it.
+
+## Import reference ACLs into MongoDB
+
+To import the above specified ACL entries from the reference file, simply
+execute the following commands.
+
+### Ensure MongoDB is running
+
+If you don't have a MongoDB server running, consider to start it within it's own
+docker container:
+
+`docker run --name mongo-acl -d mongo`
+
+Then wait until the MongoDB server is ready to accept connections. You can find
+this out by running `docker logs -f mongo-acl`. Once you see the message
+`waiting for connections on port 27017`, you can proceed with the instructions
+below.
+
+### Get mongoimport tool
+
+On Ubuntu this is a matter of `sudo apt-get install mongodb-clients`.
+
+### Import ACLs
+
+```bash
+MONGO_IP=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' mongo-acl)
+mongoimport --host $MONGO_IP --db docker_auth --collection acl < reference_acl.json
+```
+
+This should print a message like this if everything was successful:
+
+```
+connected to: 172.17.0.4
+Wed Nov 4 13:34:15.816 imported 6 objects
+```
diff --git a/examples/reference.yml b/examples/reference.yml
index 7d07efde..f8a94f01 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -71,6 +71,9 @@ ldap_auth:
base: "o=example.com"
filter: "(&(uid=${account})(objectClass=person))"
+# Authorization methods. All are tried, any one returning success is sufficient.
+# At least one must be configured.
+
# ACL specifies who can do what. If the match section of an entry matches the
# request, the set of allowed actions will be applied to the token request
# and a ticket will be issued only for those of the requested actions that are
@@ -115,3 +118,27 @@ acl:
actions: ["pull"]
comment: "Anonymous users can pull \"hello-world\"."
# Access is denied by default.
+
+# (optional) Define to query ACL from a MongoDB server.
+acl_mongo:
+ # Essentially all options are described here: https://godoc.org/gopkg.in/mgo.v2#DialInfo
+ dial_info:
+ # The MongoDB hostnames or IPs to connect to.
+ addrs: ["localhost"]
+ # The time to wait for a server to respond when first connecting and on
+ # follow up operations in the session. If timeout is zero, the call may
+ # block forever waiting for a connection to be established.
+ # (See https://golang.org/pkg/time/#ParseDuration for a format description.)
+ timeout: "10s"
+ # Database name that will be used on the MongoDB server.
+ database: "docker_auth"
+ # The username with which to connect to the MongoDB server.
+ user: ""
+ # Path to the text file with the password in it.
+ password_file: ""
+ # Name of the collection in which ACLs will be stored in MongoDB.
+ collection: "acl"
+ # Specify how long an ACL remains valid before they will be fetched again from
+ # the MongoDB server.
+ # (See https://golang.org/pkg/time/#ParseDuration for a format description.)
+ cache_ttl: "1m"
From 3fc8c289e32eecfa15bf197577378c4e6d687f02 Mon Sep 17 00:00:00 2001
From: rojer
Date: Thu, 12 Nov 2015 12:51:42 +0000
Subject: [PATCH 003/209] Add a line about MongoDB ACL to README
---
README.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/README.md b/README.md
index c36b6002..b9fd204a 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,10 @@ Supported authentication methods:
* 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
+Supported authorization methods:
+ * Static ACL
+ * MongoDB-backed ACL
+
## Installation and Examples
A public Docker image is available on Docker Hub: [cesanta/docker_auth:stable](https://registry.hub.docker.com/u/cesanta/docker_auth/).
From b836c6588a930f5d63e7ffd803f7714bd8376927 Mon Sep 17 00:00:00 2001
From: "Trevor Joynson (trevorj)"
Date: Thu, 12 Nov 2015 13:26:14 -0500
Subject: [PATCH 004/209] Utilize golang-builder for builds. Make the
build/push/tag process a bit more configurable. Support cross-compilation and
output binary compression, which defaults to false. Minimize output image by
using busybox as a base. Default cmd is now /config/auth_config.yml .
---
README.md | 1 +
auth_server/.gitignore | 1 +
auth_server/Dockerfile | 5 +++--
auth_server/Makefile | 34 ++++++++++++++++++++++++----------
auth_server/main.go | 2 +-
5 files changed, 30 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index b9fd204a..6c44ac21 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@ Supported authorization methods:
A public Docker image is available on Docker Hub: [cesanta/docker_auth:stable](https://registry.hub.docker.com/u/cesanta/docker_auth/).
The binary takes a single argument - path to the config file.
+If no arguments are given, the Dockerfile defaults to `/config/auth_config.yml`.
Example command line:
diff --git a/auth_server/.gitignore b/auth_server/.gitignore
index 24443656..e7285974 100644
--- a/auth_server/.gitignore
+++ b/auth_server/.gitignore
@@ -1 +1,2 @@
auth_server
+Godeps/
diff --git a/auth_server/Dockerfile b/auth_server/Dockerfile
index 2e89ef53..1dfd7c0c 100644
--- a/auth_server/Dockerfile
+++ b/auth_server/Dockerfile
@@ -1,4 +1,5 @@
-FROM golang:1.5
+FROM busybox
EXPOSE 5001
-ADD auth_server /
ENTRYPOINT ["/auth_server"]
+CMD ["/config/auth_config.yml"]
+COPY auth_server .
diff --git a/auth_server/Makefile b/auth_server/Makefile
index c8e856d7..97ea0aa1 100644
--- a/auth_server/Makefile
+++ b/auth_server/Makefile
@@ -1,20 +1,34 @@
-.PHONY: update-deps build docker-build
+MAKEFLAGS += --warn-undefined-variables
+IMAGE ?= cesanta/docker_auth
+COMPRESS_BINARY ?= false
+
+BUILDER_IMAGE ?= centurylink/golang-builder
+BUILDER_IMAGE_EXTRA-build-cross = -cross
+BUILDER_OPTS-docker-build = -v /var/run/docker.sock:/var/run/docker.sock
+
+.PHONY: %
all: build
+local: build-local
update-deps:
- go get -v -u -f github.com/jteeuwen/go-bindata/... .
+ go get -v -u -f github.com/tools/godep github.com/jteeuwen/go-bindata/... .
-build:
+godep:
+ godep save
+
+build-local: update-deps
go generate ./...
go build
-docker-build: update-deps build
- docker build -t cesanta/docker_auth -f Dockerfile .
+build build-cross docker-build: update-deps godep
+ docker run --rm -v $(PWD):/src -e COMPRESS_BINARY=$(COMPRESS_BINARY) $(BUILDER_OPTS-$@) $(BUILDER_IMAGE)$(BUILDER_IMAGE_EXTRA-$@) $(IMAGE)
+
+docker-tag-%:
+ docker tag -f $(IMAGE):latest $(IMAGE):$*
-docker-push-latest:
- docker push cesanta/docker_auth:latest
+docker-push-%: docker-tag-%
+ docker push $(IMAGE):$*
-docker-push-stable:
- docker tag -f cesanta/docker_auth:latest cesanta/docker_auth:stable
- docker push cesanta/docker_auth:stable
+# Shortcut for latest
+docker-push: docker-push-latest
diff --git a/auth_server/main.go b/auth_server/main.go
index 4916c79b..305fee61 100644
--- a/auth_server/main.go
+++ b/auth_server/main.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package main // import "github.com/cesanta/docker_auth/auth_server"
import (
"crypto/tls"
From a9c3c39ec8846cf6d5723a4c37f5372a4a02de13 Mon Sep 17 00:00:00 2001
From: qinglin
Date: Mon, 2 Nov 2015 15:44:22 +0800
Subject: [PATCH 005/209] Support different TLS methods for LDAP
Make InsecureSkipVerify default to false
Fixes #37
---
auth_server/authn/ldap_auth.go | 44 +++++++++++++++++++++-------------
examples/ldap_auth.yml | 10 +++++---
2 files changed, 34 insertions(+), 20 deletions(-)
diff --git a/auth_server/authn/ldap_auth.go b/auth_server/authn/ldap_auth.go
index ad24c6af..4f849f78 100755
--- a/auth_server/authn/ldap_auth.go
+++ b/auth_server/authn/ldap_auth.go
@@ -28,14 +28,15 @@ import (
)
type LDAPAuthConfig struct {
- Addr string `yaml:"addr,omitempty"`
- StartTLS bool `yaml:"tls,omitempty"`
- Base string `yaml:"base,omitempty"`
- Filter string `yaml:"filter,omitempty"`
- BindDN string `yaml:"bind_dn,omitempty"`
- BindPasswordFile string `yaml:"bind_password_file,omitempty"`
- GroupBaseDN string `yaml:"group_base_dn,omitempty"`
- GroupFilter string `yaml:"group_filter,omitempty"`
+ Addr string `yaml:"addr,omitempty"`
+ TLS string `yaml:"tls,omitempty"`
+ InsecureTLSSkipVerify bool `yaml:"insecure_tls_skip_verify,omitempty"`
+ Base string `yaml:"base,omitempty"`
+ Filter string `yaml:"filter,omitempty"`
+ BindDN string `yaml:"bind_dn,omitempty"`
+ BindPasswordFile string `yaml:"bind_password_file,omitempty"`
+ GroupBaseDN string `yaml:"group_base_dn,omitempty"`
+ GroupFilter string `yaml:"group_filter,omitempty"`
}
type LDAPAuth struct {
@@ -43,6 +44,9 @@ type LDAPAuth struct {
}
func NewLDAPAuth(c *LDAPAuthConfig) (*LDAPAuth, error) {
+ if c.TLS == "" && strings.HasSuffix(c.Addr, ":636") {
+ c.TLS = "always"
+ }
return &LDAPAuth{
config: c,
}, nil
@@ -124,18 +128,24 @@ func (la *LDAPAuth) escapeAccountInput(account string) string {
}
func (la *LDAPAuth) ldapConnection() (*ldap.Conn, error) {
- glog.V(2).Infof("Dial: starting...%s", la.config.Addr)
- l, err := ldap.Dial("tcp", fmt.Sprintf("%s", la.config.Addr))
+ var l *ldap.Conn
+ var err error
+ if la.config.TLS == "" || la.config.TLS == "none" || la.config.TLS == "starttls" {
+ glog.V(2).Infof("Dial: starting...%s", la.config.Addr)
+ l, err = ldap.Dial("tcp", fmt.Sprintf("%s", la.config.Addr))
+ if err == nil && la.config.TLS == "starttls" {
+ glog.V(2).Infof("StartTLS...")
+ if tlserr := l.StartTLS(&tls.Config{InsecureSkipVerify: la.config.InsecureTLSSkipVerify}); tlserr != nil {
+ return nil, tlserr
+ }
+ }
+ } else if la.config.TLS == "always" {
+ glog.V(2).Infof("DialTLS: starting...%s", la.config.Addr)
+ l, err = ldap.DialTLS("tcp", fmt.Sprintf("%s", la.config.Addr), &tls.Config{InsecureSkipVerify: la.config.InsecureTLSSkipVerify})
+ }
if err != nil {
return nil, err
}
- if la.config.StartTLS {
- glog.V(2).Infof("StartTLS...")
- err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
- if err != nil {
- return nil, err
- }
- }
return l, nil
}
diff --git a/examples/ldap_auth.yml b/examples/ldap_auth.yml
index 7521a2ef..3cc88ff8 100644
--- a/examples/ldap_auth.yml
+++ b/examples/ldap_auth.yml
@@ -1,13 +1,17 @@
server:
addr: :5001
- certificate: /path/to/server.pem
+ certificate: /path/to/server.pem
key: /path/to/server.key
token:
issuer: Acme auth server
expiration: 900
ldap_auth:
- addr: ldap.example.com:389
- tls: true
+ addr: ldap.example.com:636
+ # Setup tls connection method to be
+ # "" or "none": the communication won't be encrypted
+ # "always": setup LDAP over SSL/TLS
+ # "starttls": sets StartTLS as the encryption method
+ tls: always
bind_dn:
bind_password_file:
base: o=example.com
From 6eaa12afb8b332415039f746f08d96c03e0f1583 Mon Sep 17 00:00:00 2001
From: rojer
Date: Sun, 22 Nov 2015 05:27:27 +0000
Subject: [PATCH 006/209] Include CA cert bundle in the image
Busybox base image doesn't have one
---
auth_server/.gitignore | 1 +
auth_server/Dockerfile | 1 +
auth_server/Makefile | 7 ++-
auth_server/authn/bindata.go | 93 ++++++++++++++++++------------------
4 files changed, 54 insertions(+), 48 deletions(-)
diff --git a/auth_server/.gitignore b/auth_server/.gitignore
index e7285974..42f4739b 100644
--- a/auth_server/.gitignore
+++ b/auth_server/.gitignore
@@ -1,2 +1,3 @@
+ca-certificates.crt
auth_server
Godeps/
diff --git a/auth_server/Dockerfile b/auth_server/Dockerfile
index 1dfd7c0c..8c237586 100644
--- a/auth_server/Dockerfile
+++ b/auth_server/Dockerfile
@@ -2,4 +2,5 @@ FROM busybox
EXPOSE 5001
ENTRYPOINT ["/auth_server"]
CMD ["/config/auth_config.yml"]
+COPY ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY auth_server .
diff --git a/auth_server/Makefile b/auth_server/Makefile
index 97ea0aa1..d9b9bfaf 100644
--- a/auth_server/Makefile
+++ b/auth_server/Makefile
@@ -1,10 +1,12 @@
MAKEFLAGS += --warn-undefined-variables
IMAGE ?= cesanta/docker_auth
COMPRESS_BINARY ?= false
+CA_BUNDLE = /etc/ssl/certs/ca-certificates.crt
BUILDER_IMAGE ?= centurylink/golang-builder
BUILDER_IMAGE_EXTRA-build-cross = -cross
BUILDER_OPTS-docker-build = -v /var/run/docker.sock:/var/run/docker.sock
+BUILDER_IMAGE_EXTRA-docker-build =
.PHONY: %
@@ -21,7 +23,10 @@ build-local: update-deps
go generate ./...
go build
-build build-cross docker-build: update-deps godep
+ca-certificates.crt:
+ cp $(CA_BUNDLE) .
+
+build build-cross docker-build: update-deps godep ca-certificates.crt
docker run --rm -v $(PWD):/src -e COMPRESS_BINARY=$(COMPRESS_BINARY) $(BUILDER_OPTS-$@) $(BUILDER_IMAGE)$(BUILDER_IMAGE_EXTRA-$@) $(IMAGE)
docker-tag-%:
diff --git a/auth_server/authn/bindata.go b/auth_server/authn/bindata.go
index ec2adb3d..a258c62a 100644
--- a/auth_server/authn/bindata.go
+++ b/auth_server/authn/bindata.go
@@ -10,11 +10,11 @@ import (
"compress/gzip"
"fmt"
"io"
- "strings"
- "os"
- "time"
"io/ioutil"
+ "os"
"path/filepath"
+ "strings"
+ "time"
)
func bindataRead(data []byte, name string) ([]byte, error) {
@@ -43,9 +43,9 @@ type asset struct {
}
type bindataFileInfo struct {
- name string
- size int64
- mode os.FileMode
+ name string
+ size int64
+ mode os.FileMode
modTime time.Time
}
@@ -84,7 +84,7 @@ func dataGoogle_authTmpl() (*asset, error) {
}
info := bindataFileInfo{name: "data/google_auth.tmpl", size: 2796, mode: os.FileMode(420), modTime: time.Unix(1, 0)}
- a := &asset{bytes: bytes, info: info}
+ a := &asset{bytes: bytes, info: info}
return a, nil
}
@@ -107,7 +107,7 @@ func Asset(name string) ([]byte, error) {
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
- if (err != nil) {
+ if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
@@ -179,60 +179,59 @@ func AssetDir(name string) ([]string, error) {
}
type bintree struct {
- Func func() (*asset, error)
+ Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{nil, map[string]*bintree{
"data": &bintree{nil, map[string]*bintree{
- "google_auth.tmpl": &bintree{dataGoogle_authTmpl, map[string]*bintree{
- }},
+ "google_auth.tmpl": &bintree{dataGoogle_authTmpl, map[string]*bintree{}},
}},
}}
// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
- data, err := Asset(name)
- if err != nil {
- return err
- }
- info, err := AssetInfo(name)
- if err != nil {
- return err
- }
- err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
- if err != nil {
- return err
- }
- err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
- if err != nil {
- return err
- }
- err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
- if err != nil {
- return err
- }
- return nil
+ data, err := Asset(name)
+ if err != nil {
+ return err
+ }
+ info, err := AssetInfo(name)
+ if err != nil {
+ return err
+ }
+ err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
+ if err != nil {
+ return err
+ }
+ err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
+ if err != nil {
+ return err
+ }
+ err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
+ if err != nil {
+ return err
+ }
+ return nil
}
// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
- children, err := AssetDir(name)
- // File
- if err != nil {
- return RestoreAsset(dir, name)
- }
- // Dir
- for _, child := range children {
- err = RestoreAssets(dir, filepath.Join(name, child))
- if err != nil {
- return err
- }
- }
- return nil
+ children, err := AssetDir(name)
+ // File
+ if err != nil {
+ return RestoreAsset(dir, name)
+ }
+ // Dir
+ for _, child := range children {
+ err = RestoreAssets(dir, filepath.Join(name, child))
+ if err != nil {
+ return err
+ }
+ }
+ return nil
}
func _filePath(dir, name string) string {
- cannonicalName := strings.Replace(name, "\\", "/", -1)
- return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
+ cannonicalName := strings.Replace(name, "\\", "/", -1)
+ return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}
From 77670baeebbd8eb756e4e67beba09435ac6c8aa0 Mon Sep 17 00:00:00 2001
From: rojer
Date: Mon, 30 Nov 2015 02:44:08 +0000
Subject: [PATCH 007/209] Add IP matching to ACL
Fixes #49
---
auth_server/authz/acl.go | 87 ++++++++++++++++++++++++++++++++---
auth_server/authz/acl_test.go | 86 ++++++++++++++++++++++++++++++++++
auth_server/authz/authz.go | 2 +
auth_server/main.go | 2 +-
auth_server/server/server.go | 18 ++++++++
examples/reference.yml | 8 ++++
6 files changed, 195 insertions(+), 8 deletions(-)
create mode 100644 auth_server/authz/acl_test.go
diff --git a/auth_server/authz/acl.go b/auth_server/authz/acl.go
index 04f2fa8d..7865b43f 100644
--- a/auth_server/authz/acl.go
+++ b/auth_server/authz/acl.go
@@ -2,6 +2,8 @@ package authz
import (
"encoding/json"
+ "fmt"
+ "net"
"path"
"regexp"
"strings"
@@ -21,14 +23,69 @@ 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"`
+ IP *string `yaml:"ip,omitempty" json:"ip,omitempty"`
}
type aclAuthorizer struct {
acl ACL
}
+func validatePattern(p string) error {
+ if len(p) > 2 && p[0] == '/' && p[len(p)-1] == '/' {
+ _, err := regexp.Compile(p[1 : len(p)-1])
+ if err != nil {
+ return fmt.Errorf("invalid regex pattern: %s", err)
+ }
+ }
+ return nil
+}
+
+func parseIPPattern(ipp string) (*net.IPNet, error) {
+ ipnet := net.IPNet{}
+ ipnet.IP = net.ParseIP(ipp)
+ if ipnet.IP != nil {
+ if ipnet.IP.To4() != nil {
+ ipnet.Mask = net.CIDRMask(32, 32)
+ } else {
+ ipnet.Mask = net.CIDRMask(128, 128)
+ }
+ return &ipnet, nil
+ } else {
+ _, ipnet, err := net.ParseCIDR(ipp)
+ if err != nil {
+ return nil, err
+ }
+ return ipnet, nil
+ }
+}
+
+func validateMatchConditions(mc *MatchConditions) error {
+ for _, p := range []*string{mc.Account, mc.Type, mc.Name} {
+ if p == nil {
+ continue
+ }
+ err := validatePattern(*p)
+ if err != nil {
+ return fmt.Errorf("invalid pattern %q: %s", *p, err)
+ }
+ }
+ if mc.IP != nil {
+ _, err := parseIPPattern(*mc.IP)
+ if err != nil {
+ return fmt.Errorf("invalid IP pattern: %s", err)
+ }
+ }
+ return nil
+}
+
// NewACLAuthorizer Creates a new static authorizer with ACL that have been read from the config file
func NewACLAuthorizer(acl ACL) (Authorizer, error) {
+ for i, e := range acl {
+ err := validateMatchConditions(e.Match)
+ if err != nil {
+ return nil, fmt.Errorf("entry %d, invalid match conditions: %s", i, err)
+ }
+ }
glog.V(1).Infof("Created ACL Authorizer with %d entries", len(acl))
return &aclAuthorizer{acl: acl}, nil
}
@@ -78,17 +135,33 @@ func matchString(pp *string, s string, vars []string) bool {
return err == nil && matched
}
-func (e *ACLEntry) Matches(ai *AuthRequestInfo) bool {
+func matchIP(ipp *string, ip net.IP) bool {
+ if ipp == nil {
+ return true
+ }
+ if ip == nil {
+ return false
+ }
+ ipnet, err := parseIPPattern(*ipp)
+ if err != nil { // Can't happen, it supposed to have been validated
+ glog.Fatalf("Invalid IP pattern: %s", *ipp)
+ }
+ return ipnet.Contains(ip)
+}
+
+func (mc *MatchConditions) Matches(ai *AuthRequestInfo) bool {
vars := []string{
"${account}", regexp.QuoteMeta(ai.Account),
"${type}", regexp.QuoteMeta(ai.Type),
"${name}", regexp.QuoteMeta(ai.Name),
"${service}", regexp.QuoteMeta(ai.Service),
}
- if matchString(e.Match.Account, ai.Account, vars) &&
- matchString(e.Match.Type, ai.Type, vars) &&
- matchString(e.Match.Name, ai.Name, vars) {
- return true
- }
- return false
+ return matchString(mc.Account, ai.Account, vars) &&
+ matchString(mc.Type, ai.Type, vars) &&
+ matchString(mc.Name, ai.Name, vars) &&
+ matchIP(mc.IP, ai.IP)
+}
+
+func (e *ACLEntry) Matches(ai *AuthRequestInfo) bool {
+ return e.Match.Matches(ai)
}
diff --git a/auth_server/authz/acl_test.go b/auth_server/authz/acl_test.go
new file mode 100644
index 00000000..b6052460
--- /dev/null
+++ b/auth_server/authz/acl_test.go
@@ -0,0 +1,86 @@
+package authz
+
+import (
+ "net"
+ "testing"
+)
+
+func sp(s string) *string {
+ return &s
+}
+
+func TestValidation(t *testing.T) {
+ cases := []struct {
+ mc MatchConditions
+ ok bool
+ }{
+ // Valid stuff
+ {MatchConditions{}, true},
+ {MatchConditions{Account: sp("foo")}, true},
+ {MatchConditions{Account: sp("foo?*")}, true},
+ {MatchConditions{Account: sp("/foo.*/")}, true},
+ {MatchConditions{Type: sp("foo")}, true},
+ {MatchConditions{Type: sp("foo?*")}, true},
+ {MatchConditions{Type: sp("/foo.*/")}, true},
+ {MatchConditions{Name: sp("foo")}, true},
+ {MatchConditions{Name: sp("foo?*")}, true},
+ {MatchConditions{Name: sp("/foo.*/")}, true},
+ {MatchConditions{IP: sp("192.168.0.1")}, true},
+ {MatchConditions{IP: sp("192.168.0.0/16")}, true},
+ {MatchConditions{IP: sp("2001:db8::1")}, true},
+ {MatchConditions{IP: sp("2001:db8::/48")}, true},
+ // Invalid stuff
+ {MatchConditions{Account: sp("/foo?*/")}, false},
+ {MatchConditions{Type: sp("/foo?*/")}, false},
+ {MatchConditions{Name: sp("/foo?*/")}, false},
+ {MatchConditions{IP: sp("192.168.0.1/100")}, false},
+ {MatchConditions{IP: sp("192.168.0.*")}, false},
+ {MatchConditions{IP: sp("foo")}, false},
+ {MatchConditions{IP: sp("2001:db8::/222")}, false},
+ }
+ for i, c := range cases {
+ result := validateMatchConditions(&c.mc)
+ if c.ok && result != nil {
+ t.Errorf("%d: %q: expected to pass, got %s", i, c.mc, result)
+ } else if !c.ok && result == nil {
+ t.Errorf("%d: %q: expected to fail, but it passed", i, c.mc)
+ }
+ }
+}
+
+func TestMatching(t *testing.T) {
+ ai1 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz"}
+ cases := []struct {
+ mc MatchConditions
+ ai AuthRequestInfo
+ matches bool
+ }{
+ {MatchConditions{}, ai1, true},
+ {MatchConditions{Account: sp("foo")}, ai1, true},
+ {MatchConditions{Account: sp("foo"), Type: sp("bar")}, ai1, true},
+ {MatchConditions{Account: sp("foo"), Type: sp("baz")}, ai1, false},
+ {MatchConditions{Account: sp("fo?"), Type: sp("b*"), Name: sp("/z$/")}, ai1, true},
+ {MatchConditions{Account: sp("fo?"), Type: sp("b*"), Name: sp("/^z/")}, ai1, false},
+ {MatchConditions{Name: sp("${account}")}, AuthRequestInfo{Account: "foo", Name: "foo"}, true}, // Var subst
+ {MatchConditions{Name: sp("/${account}_.*/")}, AuthRequestInfo{Account: "foo", Name: "foo_x"}, true},
+ {MatchConditions{Name: sp("/${account}_.*/")}, AuthRequestInfo{Account: ".*", Name: "foo_x"}, false}, // Quoting
+ // IP matching
+ {MatchConditions{IP: sp("127.0.0.1")}, AuthRequestInfo{IP: nil}, false},
+ {MatchConditions{IP: sp("127.0.0.1")}, AuthRequestInfo{IP: net.IPv4(127, 0, 0, 1)}, true},
+ {MatchConditions{IP: sp("127.0.0.1")}, AuthRequestInfo{IP: net.IPv4(127, 0, 0, 2)}, false},
+ {MatchConditions{IP: sp("127.0.0.2")}, AuthRequestInfo{IP: net.IPv4(127, 0, 0, 1)}, false},
+ {MatchConditions{IP: sp("127.0.0.0/8")}, AuthRequestInfo{IP: net.IPv4(127, 0, 0, 1)}, true},
+ {MatchConditions{IP: sp("127.0.0.0/8")}, AuthRequestInfo{IP: net.IPv4(127, 0, 0, 2)}, true},
+ {MatchConditions{IP: sp("2001:db8::1")}, AuthRequestInfo{IP: nil}, false},
+ {MatchConditions{IP: sp("2001:db8::1")}, AuthRequestInfo{IP: net.ParseIP("2001:db8::1")}, true},
+ {MatchConditions{IP: sp("2001:db8::1")}, AuthRequestInfo{IP: net.ParseIP("2001:db8::2")}, false},
+ {MatchConditions{IP: sp("2001:db8::2")}, AuthRequestInfo{IP: net.ParseIP("2001:db8::1")}, false},
+ {MatchConditions{IP: sp("2001:db8::/48")}, AuthRequestInfo{IP: net.ParseIP("2001:db8::1")}, true},
+ {MatchConditions{IP: sp("2001:db8::/48")}, AuthRequestInfo{IP: net.ParseIP("2001:db8::2")}, true},
+ }
+ for i, c := range cases {
+ if result := c.mc.Matches(&c.ai); result != c.matches {
+ t.Errorf("%d: %#v vs %#v: expected %t, got %t", i, c.mc, c.ai, c.matches, result)
+ }
+ }
+}
diff --git a/auth_server/authz/authz.go b/auth_server/authz/authz.go
index 9b702124..3800ed42 100644
--- a/auth_server/authz/authz.go
+++ b/auth_server/authz/authz.go
@@ -3,6 +3,7 @@ package authz
import (
"errors"
"fmt"
+ "net"
"strings"
)
@@ -36,6 +37,7 @@ type AuthRequestInfo struct {
Type string
Name string
Service string
+ IP net.IP
Actions []string
}
diff --git a/auth_server/main.go b/auth_server/main.go
index 305fee61..086393ed 100644
--- a/auth_server/main.go
+++ b/auth_server/main.go
@@ -87,7 +87,7 @@ func ServeOnce(c *server.Config, cf string, hd *httpdown.HTTP) (*server.AuthServ
if err != nil {
glog.Exitf("Failed to set up listener: %s", err)
}
- glog.Infof("Serving")
+ glog.Infof("Serving on %s", c.Server.ListenAddress)
return as, s
}
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 7ea4873b..b7ec178b 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -21,6 +21,7 @@ import (
"encoding/json"
"fmt"
"math/rand"
+ "net"
"net/http"
"sort"
"strings"
@@ -90,8 +91,25 @@ func NewAuthServer(c *Config) (*AuthServer, error) {
return as, nil
}
+func parseRemoteAddr(ra string) net.IP {
+ colonIndex := strings.LastIndex(ra, ":")
+ if colonIndex == -1 {
+ return nil
+ }
+ ra = ra[:colonIndex]
+ if ra[0] == '[' && ra[len(ra)-1] == ']' { // IPv6
+ ra = ra[1 : len(ra)-1]
+ }
+ res := net.ParseIP(ra)
+ return res
+}
+
func (as *AuthServer) ParseRequest(req *http.Request) (*AuthRequest, error) {
ar := &AuthRequest{RemoteAddr: req.RemoteAddr}
+ ar.ai.IP = parseRemoteAddr(req.RemoteAddr)
+ if ar.ai.IP == nil {
+ return nil, fmt.Errorf("unable to parse remote addr %s", req.RemoteAddr)
+ }
user, password, haveBasicAuth := req.BasicAuth()
if haveBasicAuth {
ar.User = user
diff --git a/examples/reference.yml b/examples/reference.yml
index f8a94f01..8b3a1ea8 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -85,6 +85,7 @@ ldap_auth:
# so "foobar", "f??bar", "f*bar" are all valid. For even more flexibility
# match patterns can be evaluated as regexes by enclosing them in //, e.g.
# "/(foo|bar)/".
+# * IP match can be single IP address or a subnet in the "prefix/mask" notation.
# * ACL is evaluated in the order it is defined until a match is found.
# * Empty match clause matches anything, it only makes sense at the end of the
# list and can be used as a way of specifying default permissions.
@@ -99,6 +100,13 @@ ldap_auth:
# * ${type} - the type of the entity, normally "repository".
# * ${name} - the name of the repository (i.e. image), e.g. centos.
acl:
+ # Any manipulations from localhost are allowed.
+ - match: {ip: "127.0.0.0/8"}
+ actions: ["*"]
+ comment: "Allow everything from localhost (IPv4)"
+ - match: {ip: "::1"}
+ actions: ["*"]
+ comment: "Allow everything from localhost (IPv6)"
- match: {account: "admin"}
actions: ["*"]
comment: "Admin has full access to everything."
From ad88cca0bf5d678cd1f9415c13ae4bcc1e86aef6 Mon Sep 17 00:00:00 2001
From: Carson Anderson
Date: Fri, 11 Dec 2015 17:18:04 -0700
Subject: [PATCH 008/209] move docker-build outside of normal build target This
makes it much easier to rebuild the project by itself if you don't need
updated deps or the full image
---
auth_server/Makefile | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/auth_server/Makefile b/auth_server/Makefile
index d9b9bfaf..b1d66d2d 100644
--- a/auth_server/Makefile
+++ b/auth_server/Makefile
@@ -26,9 +26,11 @@ build-local: update-deps
ca-certificates.crt:
cp $(CA_BUNDLE) .
-build build-cross docker-build: update-deps godep ca-certificates.crt
+docker-build:
docker run --rm -v $(PWD):/src -e COMPRESS_BINARY=$(COMPRESS_BINARY) $(BUILDER_OPTS-$@) $(BUILDER_IMAGE)$(BUILDER_IMAGE_EXTRA-$@) $(IMAGE)
+build build-cross: update-deps godep ca-certificates.crt docker-build
+
docker-tag-%:
docker tag -f $(IMAGE):latest $(IMAGE):$*
From 4fde76fbc8c1ba804fd01a8780bccc03e776c68c Mon Sep 17 00:00:00 2001
From: rojer
Date: Wed, 16 Dec 2015 02:19:55 +0000
Subject: [PATCH 009/209] Fix reference config and clarify ACL documentation
Closes #56
---
examples/reference.yml | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/examples/reference.yml b/examples/reference.yml
index 8b3a1ea8..c669fb6a 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -87,6 +87,8 @@ ldap_auth:
# "/(foo|bar)/".
# * IP match can be single IP address or a subnet in the "prefix/mask" notation.
# * ACL is evaluated in the order it is defined until a match is found.
+# Rules below the first match are not evaluated, so you'll need to put more
+# specific rules above more broad ones.
# * Empty match clause matches anything, it only makes sense at the end of the
# list and can be used as a way of specifying default permissions.
# * Empty actions set means "deny everything". Thus, a rule with `actions: []`
@@ -116,12 +118,12 @@ acl:
- match: {account: "test"}
actions: []
comment: "User \"test\" has full access to test-* images but nothing else. (2)"
- - match: {account: "/.+/"}
- actions: ["pull"]
- comment: "All logged in users can pull all images."
- match: {account: "/.+/", name: "${account}/*"}
actions: ["*"]
- comment: "All logged in users can push all images that are in a namespace beginning with their name"
+ comment: "Logged in users have full access to images that are in their 'namespace'"
+ - match: {account: "/.+/"}
+ actions: ["pull"]
+ comment: "Logged in users can pull all images."
- match: {account: "", name: "hello-world"}
actions: ["pull"]
comment: "Anonymous users can pull \"hello-world\"."
From fcfc5c9ef0ce0eb6e589e46980dbf06400ed936e Mon Sep 17 00:00:00 2001
From: Carson Anderson
Date: Fri, 11 Dec 2015 09:52:27 -0700
Subject: [PATCH 010/209] Add MongoDB Auth support
Uses standardized mongo config, sessions, and copys
Update docs concerning MongoDB
gofmt all
Use separate configs for mongo_auth and acl_mongo
---
README.md | 1 +
auth_server/Makefile | 2 +-
auth_server/authn/mongo_auth.go | 114 ++++++++++++++++++
auth_server/authz/acl_mongo.go | 90 ++++++--------
auth_server/mgo_session/mgo_session.go | 72 +++++++++++
auth_server/server/config.go | 28 +++--
auth_server/server/server.go | 11 +-
..._Backend_MongoDB.md => Backend_MongoDB.md} | 33 +++--
examples/reference.yml | 21 ++++
9 files changed, 296 insertions(+), 76 deletions(-)
create mode 100644 auth_server/authn/mongo_auth.go
create mode 100644 auth_server/mgo_session/mgo_session.go
rename docs/{ACL_Backend_MongoDB.md => Backend_MongoDB.md} (81%)
diff --git a/README.md b/README.md
index 6c44ac21..6ba29541 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/auth_server/Makefile b/auth_server/Makefile
index d9b9bfaf..55f582d0 100644
--- a/auth_server/Makefile
+++ b/auth_server/Makefile
@@ -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
diff --git a/auth_server/authn/mongo_auth.go b/auth_server/authn/mongo_auth.go
new file mode 100644
index 00000000..7528083e
--- /dev/null
+++ b/auth_server/authn/mongo_auth.go
@@ -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"
+}
diff --git a/auth_server/authz/acl_mongo.go b/auth_server/authz/acl_mongo.go
index e722bdc3..2e03134d 100644
--- a/auth_server/authz/acl_mongo.go
+++ b/auth_server/authz/acl_mongo.go
@@ -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
}
@@ -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()
@@ -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
diff --git a/auth_server/mgo_session/mgo_session.go b/auth_server/mgo_session/mgo_session.go
new file mode 100644
index 00000000..5d1d2ba2
--- /dev/null
+++ b/auth_server/mgo_session/mgo_session.go
@@ -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
+}
diff --git a/auth_server/server/config.go b/auth_server/server/config.go
index d9575390..6e363be7 100644
--- a/auth_server/server/config.go
+++ b/auth_server/server/config.go
@@ -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 {
@@ -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)
@@ -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
}
}
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index b7ec178b..b6f4d313 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -63,8 +63,8 @@ func NewAuthServer(c *Config) (*AuthServer, error) {
}
as.authorizers = append(as.authorizers, staticAuthorizer)
}
- if c.ACLMongoConf != nil {
- mongoAuthorizer, err := authz.NewACLMongoAuthorizer(*c.ACLMongoConf)
+ if c.ACLMongo != nil {
+ mongoAuthorizer, err := authz.NewACLMongoAuthorizer(c.ACLMongo)
if err != nil {
return nil, err
}
@@ -88,6 +88,13 @@ func NewAuthServer(c *Config) (*AuthServer, error) {
}
as.authenticators = append(as.authenticators, la)
}
+ if c.MongoAuth != nil {
+ ma, err := authn.NewMongoAuth(c.MongoAuth)
+ if err != nil {
+ return nil, err
+ }
+ as.authenticators = append(as.authenticators, ma)
+ }
return as, nil
}
diff --git a/docs/ACL_Backend_MongoDB.md b/docs/Backend_MongoDB.md
similarity index 81%
rename from docs/ACL_Backend_MongoDB.md
rename to docs/Backend_MongoDB.md
index 281636eb..bde08cad 100644
--- a/docs/ACL_Backend_MongoDB.md
+++ b/docs/Backend_MongoDB.md
@@ -1,15 +1,30 @@
-# ACL backend in MongoDB
+# MongoDB Backends
-Maybe you want to manage your ACL from an external application and therefore
+You may want to manage your ACLs and Users from an external application and therefore
need them to be stored outside of your auth_server's configuration file.
-For this purpose, there's a [MongoDB](https://www.mongodb.org/) ACL backend
-which can query an ACL from a MongoDB database.
+For this purpose, there's a [MongoDB](https://www.mongodb.org/) backend
+which can query ACL and Auth from a MongoDB database.
+
+
+## Auth backend in MongoDB
+
+Auth entries in mongo are single dictionary containing a username and password entry.
+The password entry must contain a BCrypt hash.
+
+```json
+{
+ "username" : "admin",
+ "password" : "$2y$05$B.x046DV3bvuwFgn0I42F.W/SbRU5fUoCbCGtjFl7S33aCUHNBxbq"
+}
+```
+
+## ACL backend in MongoDB
A typical ACL entry from the static YAML configuration file looks something like
this:
-```
+```yaml
- match: {account: "/.+/", name: "${account}/*"}
actions: ["push", "pull"]
comment: "All logged in users can push all images that are in a namespace beginning with their name"
@@ -37,12 +52,12 @@ be imported into MongoDB. Those ACL entries reflect what's specified in the
**Note** that each document entry must span exactly one line or otherwise the
`mongoimport` tool (see below) will not accept it.
-## Import reference ACLs into MongoDB
+### Import reference ACLs into MongoDB
To import the above specified ACL entries from the reference file, simply
execute the following commands.
-### Ensure MongoDB is running
+#### Ensure MongoDB is running
If you don't have a MongoDB server running, consider to start it within it's own
docker container:
@@ -54,11 +69,11 @@ this out by running `docker logs -f mongo-acl`. Once you see the message
`waiting for connections on port 27017`, you can proceed with the instructions
below.
-### Get mongoimport tool
+#### Get mongoimport tool
On Ubuntu this is a matter of `sudo apt-get install mongodb-clients`.
-### Import ACLs
+#### Import ACLs
```bash
MONGO_IP=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' mongo-acl)
diff --git a/examples/reference.yml b/examples/reference.yml
index 8b3a1ea8..4bc90cd9 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -71,6 +71,27 @@ ldap_auth:
base: "o=example.com"
filter: "(&(uid=${account})(objectClass=person))"
+mongo_auth:
+ # Essentially all options are described here: https://godoc.org/gopkg.in/mgo.v2#DialInfo
+ dial_info:
+ # The MongoDB hostnames or IPs to connect to.
+ addrs: ["localhost"]
+ # The time to wait for a server to respond when first connecting and on
+ # follow up operations in the session. If timeout is zero, the call may
+ # block forever waiting for a connection to be established.
+ # (See https://golang.org/pkg/time/#ParseDuration for a format description.)
+ timeout: "10s"
+ # Database name that will be used on the MongoDB server.
+ database: "docker_auth"
+ # The username with which to connect to the MongoDB server.
+ user: ""
+ # Path to the text file with the password in it.
+ password_file: ""
+ # Name of the collection in which ACLs will be stored in MongoDB.
+ collection: "users"
+ # Unlike acl_mongo we don't cache the full user set. We just query mongo for
+ # an exact match for each authorization
+
# Authorization methods. All are tried, any one returning success is sufficient.
# At least one must be configured.
From 06086f3e5e7d23d4bf2d102edb97cfc0d7d214cb Mon Sep 17 00:00:00 2001
From: Carson Anderson
Date: Fri, 18 Dec 2015 14:33:25 -0700
Subject: [PATCH 011/209] Require seq field in mongo ACls
---
auth_server/authz/acl_mongo.go | 56 ++++++++++++++++++++++++++++------
docs/Backend_MongoDB.md | 17 +++++++----
2 files changed, 58 insertions(+), 15 deletions(-)
diff --git a/auth_server/authz/acl_mongo.go b/auth_server/authz/acl_mongo.go
index 2e03134d..0f2181f3 100644
--- a/auth_server/authz/acl_mongo.go
+++ b/auth_server/authz/acl_mongo.go
@@ -1,6 +1,7 @@
package authz
import (
+ "errors"
"fmt"
"sync"
"time"
@@ -11,6 +12,13 @@ import (
"gopkg.in/mgo.v2/bson"
)
+type MongoACL []MongoACLEntry
+
+type MongoACLEntry struct {
+ ACLEntry `bson:",inline"`
+ Seq *int
+}
+
type ACLMongoConfig struct {
MongoConfig *mgo_session.Config `yaml:"dial_info,omitempty"`
Collection string `yaml:"collection,omitempty"`
@@ -28,9 +36,9 @@ type aclMongoAuthorizer struct {
CacheTTL time.Duration `yaml:"cache_ttl,omitempty"`
}
-// NewACLMongoAuthorizer creates a new ACL Mongo authorizer
+// NewACLMongoAuthorizer creates a new ACL MongoDB authorizer
func NewACLMongoAuthorizer(c *ACLMongoConfig) (Authorizer, error) {
- // Attempt to create new mongo session.
+ // Attempt to create new MongoDB session.
session, err := mgo_session.New(c.MongoConfig)
if err != nil {
return nil, err
@@ -67,7 +75,7 @@ func (ma *aclMongoAuthorizer) Authorize(ai *AuthRequestInfo) ([]string, error) {
// 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.
+ //First validate the MongoDB config.
if err := c.MongoConfig.Validate(configKey); err != nil {
return err
}
@@ -119,7 +127,7 @@ func (ma *aclMongoAuthorizer) continuouslyUpdateACLCache() {
func (ma *aclMongoAuthorizer) updateACLCache() error {
// Get ACL from MongoDB
- var newACL ACL
+ var newACL MongoACL
// Copy our session
tmp_session := ma.session.Copy()
@@ -128,13 +136,43 @@ func (ma *aclMongoAuthorizer) updateACLCache() error {
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 {
+
+ // Create sequence index obj
+ index := mgo.Index{
+ Key: []string{"seq"},
+ Unique: true,
+ DropDups: false, // Error on duplicate key document instead of drop.
+ }
+
+ // Enforce a sequence index. This is fine to do frequently per the docs:
+ // https://godoc.org/gopkg.in/mgo.v2#Collection.EnsureIndex:
+ // Once EnsureIndex returns successfully, following requests for the same index
+ // will not contact the server unless Collection.DropIndex is used to drop the same
+ // index, or Session.ResetIndexCache is called.
+ if err := collection.EnsureIndex(index); err != nil {
return err
}
+
+ // Get all ACLs that have the required key
+ if err := collection.Find(bson.M{}).Sort("seq").All(&newACL); err != nil {
+ return err
+ }
+
glog.V(2).Infof("Number of new ACL entries from MongoDB: %d", len(newACL))
- newStaticAuthorizer, err := NewACLAuthorizer(newACL)
+ // It is possible that the top document in the collection exists with a nil Seq.
+ // if that's true we pull it out of the slice and complain about it.
+ if len(newACL) > 0 && newACL[0].Seq == nil {
+ topACL := newACL[0]
+ return errors.New(fmt.Sprintf("Seq not set for ACL entry: %+v", topACL))
+ }
+
+ var retACL ACL
+ for _, e := range newACL {
+ retACL = append(retACL, e.ACLEntry)
+ }
+
+ newStaticAuthorizer, err := NewACLAuthorizer(retACL)
if err != nil {
return err
}
@@ -144,7 +182,7 @@ func (ma *aclMongoAuthorizer) updateACLCache() error {
ma.staticAuthorizer = newStaticAuthorizer
ma.lock.Unlock()
- glog.V(2).Infof("Got new ACL from MongoDB: %s", newACL)
- glog.V(1).Infof("Installed new ACL from MongoDB (%d entries)", len(newACL))
+ glog.V(2).Infof("Got new ACL from MongoDB: %s", retACL)
+ glog.V(1).Infof("Installed new ACL from MongoDB (%d entries)", len(retACL))
return nil
}
diff --git a/docs/Backend_MongoDB.md b/docs/Backend_MongoDB.md
index bde08cad..54a13e51 100644
--- a/docs/Backend_MongoDB.md
+++ b/docs/Backend_MongoDB.md
@@ -38,15 +38,20 @@ MongoDB quite easily. Below you can find a list of ACL entries that are ready to
be imported into MongoDB. Those ACL entries reflect what's specified in the
`example/reference.yml` file under the `acl` section (aka static ACL).
+The added field of seq is used to provide a reliable order which MongoDB does not
+guarantee by default, i.e. [Natural Sorting](https://docs.mongodb.org/manual/reference/method/cursor.sort/#return-natural-order).
+
+``seq`` is a required field in all MongoDB ACL documents. Any documents without this key will be excluded. seq uniqeness is also enforced.
+
**reference_acl.json**
```json
-{"match" : {"account" : "admin"}, "actions" : ["*"], "comment" : "Admin has full access to everything."}
-{"match" : {"account" : "test", "name" : "test-*"}, "actions" : ["*"], "comment" : "User \"test\" has full access to test-* images but nothing else. (1)"}
-{"match" : {"account" : "test"}, "actions" : [], "comment" : "User \"test\" has full access to test-* images but nothing else. (2)"}
-{"match" : {"account" : "/.+/"}, "actions" : ["pull"], "comment" : "All logged in users can pull all images."}
-{"match" : {"account" : "/.+/", "name" : "${account}/*"}, "actions" : ["*"], "comment" : "All logged in users can push all images that are in a namespace beginning with their name"}
-{"match" : {"account" : "", "name" : "hello-world"}, "actions" : ["pull"], "comment" : "Anonymous users can pull \"hello-world\"."}
+{"seq": 10, "match" : {"account" : "admin"}, "actions" : ["*"], "comment" : "Admin has full access to everything."}
+{"seq": 20, "match" : {"account" : "test", "name" : "test-*"}, "actions" : ["*"], "comment" : "User \"test\" has full access to test-* images but nothing else. (1)"}
+{"seq": 30, "match" : {"account" : "test"}, "actions" : [], "comment" : "User \"test\" has full access to test-* images but nothing else. (2)"}
+{"seq": 40, "match" : {"account" : "/.+/"}, "actions" : ["pull"], "comment" : "All logged in users can pull all images."}
+{"seq": 50, "match" : {"account" : "/.+/", "name" : "${account}/*"}, "actions" : ["*"], "comment" : "All logged in users can push all images that are in a namespace beginning with their name"}
+{"seq": 60, "match" : {"account" : "", "name" : "hello-world"}, "actions" : ["pull"], "comment" : "Anonymous users can pull \"hello-world\"."}
```
**Note** that each document entry must span exactly one line or otherwise the
From 67d2e584c733403e4b4e258f6279eff8963148de Mon Sep 17 00:00:00 2001
From: Carson Anderson
Date: Wed, 10 Feb 2016 16:56:26 -0700
Subject: [PATCH 012/209] Enforce index in users collection
---
auth_server/authn/mongo_auth.go | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/auth_server/authn/mongo_auth.go b/auth_server/authn/mongo_auth.go
index 7528083e..09ab5aad 100644
--- a/auth_server/authn/mongo_auth.go
+++ b/auth_server/authn/mongo_auth.go
@@ -49,6 +49,30 @@ func NewMongoAuth(c *MongoAuthConfig) (*MongoAuth, error) {
return nil, err
}
+ // Copy our session
+ tmp_session := session.Copy()
+ // Close up when we are done
+ defer tmp_session.Close()
+
+ // determine collection
+ collection := tmp_session.DB(c.MongoConfig.DialInfo.Database).C(c.Collection)
+
+ // Create username index obj
+ index := mgo.Index{
+ Key: []string{"username"},
+ Unique: true,
+ DropDups: false, // Error on duplicate key document instead of drop.
+ }
+
+ // Enforce a username index. This is fine to do frequently per the docs:
+ // https://godoc.org/gopkg.in/mgo.v2#Collection.EnsureIndex:
+ // Once EnsureIndex returns successfully, following requests for the same index
+ // will not contact the server unless Collection.DropIndex is used to drop the same
+ // index, or Session.ResetIndexCache is called.
+ if err := collection.EnsureIndex(index); err != nil {
+ return nil, err
+ }
+
return &MongoAuth{
config: c,
session: session,
From 10623438cf8794052a05a01dfab31ebd43131b55 Mon Sep 17 00:00:00 2001
From: rojer
Date: Thu, 11 Feb 2016 21:09:25 +0000
Subject: [PATCH 013/209] Update README.md
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 6ba29541..faca98f8 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-Docker Registry 2.0 authentication server
+Docker Registry 2 authentication server
=========================================
The original Docker Registry server (v1) did not provide any support for authentication or authorization.
@@ -34,7 +34,7 @@ $ docker run \
--rm -it --name docker_auth -p 5001:5001 \
-v /path/to/config_dir:/config:ro \
-v /var/log/docker_auth:/logs \
- cesanta/docker_auth /config/auth_config.yml
+ cesanta/docker_auth:stable /config/auth_config.yml
```
See the [example config files](https://github.com/cesanta/docker_auth/tree/master/examples/) to get an idea of what is possible.
@@ -43,7 +43,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 --alsologtostderr /config/auth_config.yml
+docker run ... cesanta/docker_auth:stable --v=2 --alsologtostderr /config/auth_config.yml
```
## Contributing
From b92d9f2a573a329564f5958aff2598366fbb7a29 Mon Sep 17 00:00:00 2001
From: rojer
Date: Sat, 13 Feb 2016 09:18:28 +0000
Subject: [PATCH 014/209] Add example with 172.17.0.1
This also serves as an example for single ipv4 match
Fixes #64
---
examples/reference.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/examples/reference.yml b/examples/reference.yml
index 97bd4af3..ee0a8cd5 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -123,13 +123,15 @@ mongo_auth:
# * ${type} - the type of the entity, normally "repository".
# * ${name} - the name of the repository (i.e. image), e.g. centos.
acl:
- # Any manipulations from localhost are allowed.
- match: {ip: "127.0.0.0/8"}
actions: ["*"]
comment: "Allow everything from localhost (IPv4)"
- match: {ip: "::1"}
actions: ["*"]
comment: "Allow everything from localhost (IPv6)"
+ - match: {ip: "172.17.0.1"}
+ actions: ["*"]
+ comment: "Allow everything from the local Docker bridge address"
- match: {account: "admin"}
actions: ["*"]
comment: "Admin has full access to everything."
From 25b484b308719f2426ee869c205e667e0f604d2f Mon Sep 17 00:00:00 2001
From: rojer
Date: Thu, 25 Feb 2016 20:57:06 +0000
Subject: [PATCH 015/209] Add support for multiple scopes
Fixes #67
---
auth_server/server/server.go | 146 +++++++++++++++++++++++------------
1 file changed, 96 insertions(+), 50 deletions(-)
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index b6f4d313..3aa8abbc 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -33,17 +33,6 @@ import (
"github.com/golang/glog"
)
-type AuthRequest struct {
- RemoteAddr string
- User string
- Password authn.PasswordString
- ai authz.AuthRequestInfo
-}
-
-func (ar AuthRequest) String() string {
- return fmt.Sprintf("{%s:%s@%s %s}", ar.User, ar.Password, ar.RemoteAddr, ar.ai)
-}
-
type AuthServer struct {
config *Config
authenticators []authn.Authenticator
@@ -98,6 +87,31 @@ func NewAuthServer(c *Config) (*AuthServer, error) {
return as, nil
}
+type authRequest struct {
+ RemoteAddr string
+ RemoteIP net.IP
+ User string
+ Password authn.PasswordString
+ Account string
+ Service string
+ Scopes []authScope
+}
+
+type authScope struct {
+ Type string
+ Name string
+ Actions []string
+}
+
+type authzResult struct {
+ scope authScope
+ autorizedActions []string
+}
+
+func (ar authRequest) String() string {
+ return fmt.Sprintf("{%s:%s@%s %s}", ar.User, ar.Password, ar.RemoteAddr, ar.Scopes)
+}
+
func parseRemoteAddr(ra string) net.IP {
colonIndex := strings.LastIndex(ra, ":")
if colonIndex == -1 {
@@ -111,10 +125,10 @@ func parseRemoteAddr(ra string) net.IP {
return res
}
-func (as *AuthServer) ParseRequest(req *http.Request) (*AuthRequest, error) {
- ar := &AuthRequest{RemoteAddr: req.RemoteAddr}
- ar.ai.IP = parseRemoteAddr(req.RemoteAddr)
- if ar.ai.IP == nil {
+func (as *AuthServer) ParseRequest(req *http.Request) (*authRequest, error) {
+ ar := &authRequest{RemoteAddr: req.RemoteAddr}
+ ar.RemoteIP = parseRemoteAddr(req.RemoteAddr)
+ if ar.RemoteIP == nil {
return nil, fmt.Errorf("unable to parse remote addr %s", req.RemoteAddr)
}
user, password, haveBasicAuth := req.BasicAuth()
@@ -122,31 +136,36 @@ func (as *AuthServer) ParseRequest(req *http.Request) (*AuthRequest, error) {
ar.User = user
ar.Password = authn.PasswordString(password)
}
- ar.ai.Account = req.FormValue("account")
- if ar.ai.Account == "" {
- ar.ai.Account = ar.User
- } else if haveBasicAuth && ar.ai.Account != ar.User {
- return nil, fmt.Errorf("user and account are not the same (%q vs %q)", ar.User, ar.ai.Account)
+ ar.Account = req.FormValue("account")
+ if ar.Account == "" {
+ ar.Account = ar.User
+ } else if haveBasicAuth && ar.Account != ar.User {
+ return nil, fmt.Errorf("user and account are not the same (%q vs %q)", ar.User, ar.Account)
+ }
+ ar.Service = req.FormValue("service")
+ if err := req.ParseForm(); err != nil {
+ return nil, fmt.Errorf("invalid form value")
}
- ar.ai.Service = req.FormValue("service")
- scope := req.FormValue("scope")
- if scope != "" {
- parts := strings.Split(scope, ":")
+ for _, scopeStr := range req.Form["scope"] {
+ parts := strings.Split(scopeStr, ":")
if len(parts) != 3 {
- return nil, fmt.Errorf("invalid scope: %q", scope)
+ return nil, fmt.Errorf("invalid scope: %q", scopeStr)
}
- ar.ai.Type = parts[0]
- ar.ai.Name = parts[1]
- ar.ai.Actions = strings.Split(parts[2], ",")
- sort.Strings(ar.ai.Actions)
+ scope := authScope{
+ Type: parts[0],
+ Name: parts[1],
+ Actions: strings.Split(parts[2], ","),
+ }
+ sort.Strings(scope.Actions)
+ ar.Scopes = append(ar.Scopes, scope)
}
return ar, nil
}
-func (as *AuthServer) Authenticate(ar *AuthRequest) (bool, error) {
+func (as *AuthServer) Authenticate(ar *authRequest) (bool, error) {
for i, a := range as.authenticators {
- result, err := a.Authenticate(ar.ai.Account, ar.Password)
- glog.V(2).Infof("Authn %s %s -> %t, %s", a.Name(), ar.ai.Account, result, err)
+ result, err := a.Authenticate(ar.Account, ar.Password)
+ glog.V(2).Infof("Authn %s %s -> %t, %s", a.Name(), ar.Account, result, err)
if err != nil {
if err == authn.NoMatch {
continue
@@ -158,31 +177,51 @@ func (as *AuthServer) Authenticate(ar *AuthRequest) (bool, error) {
return result, nil
}
// Deny by default.
- glog.Warningf("%s did not match any authn rule", ar.ai)
+ glog.Warningf("%s did not match any authn rule", ar)
return false, nil
}
-func (as *AuthServer) Authorize(ar *AuthRequest) ([]string, error) {
+func (as *AuthServer) authorizeScope(ai *authz.AuthRequestInfo) ([]string, error) {
for i, a := range as.authorizers {
- result, err := a.Authorize(&ar.ai)
- glog.V(2).Infof("Authz %s %s -> %s, %s", a.Name(), ar.ai, result, err)
+ result, err := a.Authorize(ai)
+ glog.V(2).Infof("Authz %s %s -> %s, %s", a.Name(), *ai, result, err)
if err != nil {
if err == authz.NoMatch {
continue
}
err = fmt.Errorf("authz #%d returned error: %s", i+1, err)
- glog.Errorf("%s: %s", ar, err)
- return nil, authz.NoMatch
+ glog.Errorf("%s: %s", *ai, err)
+ return nil, err
}
return result, nil
}
// Deny by default.
- glog.Warningf("%s did not match any authz rule", ar.ai)
+ glog.Warningf("%s did not match any authz rule", *ai)
return nil, nil
}
+func (as *AuthServer) Authorize(ar *authRequest) ([]authzResult, error) {
+ ares := []authzResult{}
+ for _, scope := range ar.Scopes {
+ ai := &authz.AuthRequestInfo{
+ Account: ar.Account,
+ Type: scope.Type,
+ Name: scope.Name,
+ Service: ar.Service,
+ IP: ar.RemoteIP,
+ Actions: scope.Actions,
+ }
+ actions, err := as.authorizeScope(ai)
+ if err != nil {
+ return nil, err
+ }
+ ares = append(ares, authzResult{scope: scope, autorizedActions: actions})
+ }
+ return ares, nil
+}
+
// https://github.com/docker/distribution/blob/master/docs/spec/auth/token.md#example
-func (as *AuthServer) CreateToken(ar *AuthRequest, actions []string) (string, error) {
+func (as *AuthServer) CreateToken(ar *authRequest, ares []authzResult) (string, error) {
now := time.Now().Unix()
tc := &as.config.Token
@@ -203,18 +242,25 @@ func (as *AuthServer) CreateToken(ar *AuthRequest, actions []string) (string, er
claims := token.ClaimSet{
Issuer: tc.Issuer,
- Subject: ar.ai.Account,
- Audience: ar.ai.Service,
+ Subject: ar.Account,
+ Audience: ar.Service,
NotBefore: now - 1,
IssuedAt: now,
Expiration: now + tc.Expiration,
JWTID: fmt.Sprintf("%d", rand.Int63()),
Access: []*token.ResourceActions{},
}
- if len(actions) > 0 {
- claims.Access = []*token.ResourceActions{
- &token.ResourceActions{Type: ar.ai.Type, Name: ar.ai.Name, Actions: actions},
+ for _, a := range ares {
+ ra := &token.ResourceActions{
+ Type: a.scope.Type,
+ Name: a.scope.Name,
+ Actions: a.autorizedActions,
+ }
+ if ra.Actions == nil {
+ ra.Actions = []string{}
}
+ sort.Strings(ra.Actions)
+ claims.Access = append(claims.Access, ra)
}
claimsJSON, err := json.Marshal(claims)
if err != nil {
@@ -257,7 +303,7 @@ func (as *AuthServer) doIndex(rw http.ResponseWriter, req *http.Request) {
func (as *AuthServer) doAuth(rw http.ResponseWriter, req *http.Request) {
ar, err := as.ParseRequest(req)
- authorizedActions := []string{}
+ ares := []authzResult{}
if err != nil {
glog.Warningf("Bad request: %s", err)
http.Error(rw, fmt.Sprintf("Bad request: %s", err), http.StatusBadRequest)
@@ -276,8 +322,8 @@ func (as *AuthServer) doAuth(rw http.ResponseWriter, req *http.Request) {
return
}
}
- if len(ar.ai.Actions) > 0 {
- authorizedActions, err = as.Authorize(ar)
+ if len(ar.Scopes) > 0 {
+ ares, err = as.Authorize(ar)
if err != nil {
http.Error(rw, fmt.Sprintf("Authorization failed (%s)", err), http.StatusInternalServerError)
return
@@ -285,7 +331,7 @@ func (as *AuthServer) doAuth(rw http.ResponseWriter, req *http.Request) {
} else {
// Authenticaltion-only request ("docker login"), pass through.
}
- token, err := as.CreateToken(ar, authorizedActions)
+ token, err := as.CreateToken(ar, ares)
if err != nil {
msg := fmt.Sprintf("Failed to generate token %s", err)
http.Error(rw, msg, http.StatusInternalServerError)
@@ -293,7 +339,7 @@ func (as *AuthServer) doAuth(rw http.ResponseWriter, req *http.Request) {
return
}
result, _ := json.Marshal(&map[string]string{"token": token})
- glog.V(2).Infof("%s", result)
+ glog.V(3).Infof("%s", result)
rw.Header().Set("Content-Type", "application/json")
rw.Write(result)
}
From 52847f505a29c65010b3d508c7b82cd94bd4175c Mon Sep 17 00:00:00 2001
From: jgsqware
Date: Wed, 9 Mar 2016 07:52:00 +0100
Subject: [PATCH 016/209] Make ACLMongo omitempty for serialization
---
auth_server/server/config.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/auth_server/server/config.go b/auth_server/server/config.go
index 6e363be7..1a0addc5 100644
--- a/auth_server/server/config.go
+++ b/auth_server/server/config.go
@@ -38,7 +38,7 @@ type Config struct {
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"`
+ ACLMongo *authz.ACLMongoConfig `yaml:"acl_mongo,omitempty"`
}
type ServerConfig struct {
From 9dfadba7e6d78d0b46e09e53ef16046e0eea79a2 Mon Sep 17 00:00:00 2001
From: Gaurav Bhandare
Date: Tue, 22 Mar 2016 09:44:55 -0400
Subject: [PATCH 017/209] Making NotBefore value as now-10
---
auth_server/server/server.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 3aa8abbc..c96ae41b 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -244,7 +244,7 @@ func (as *AuthServer) CreateToken(ar *authRequest, ares []authzResult) (string,
Issuer: tc.Issuer,
Subject: ar.Account,
Audience: ar.Service,
- NotBefore: now - 1,
+ NotBefore: now - 10,
IssuedAt: now,
Expiration: now + tc.Expiration,
JWTID: fmt.Sprintf("%d", rand.Int63()),
From c4cb4acead7fc056a7b45e738548390750d7f61c Mon Sep 17 00:00:00 2001
From: Carson Anderson
Date: Wed, 23 Mar 2016 13:21:25 -0600
Subject: [PATCH 018/209] Retry connection to mongo when we get a EOF error.
---
auth_server/authn/mongo_auth.go | 17 +++++++++++++++++
auth_server/authz/acl_mongo.go | 25 ++++++++++++++++---------
2 files changed, 33 insertions(+), 9 deletions(-)
diff --git a/auth_server/authn/mongo_auth.go b/auth_server/authn/mongo_auth.go
index 09ab5aad..4fd36d25 100644
--- a/auth_server/authn/mongo_auth.go
+++ b/auth_server/authn/mongo_auth.go
@@ -17,7 +17,10 @@
package authn
import (
+ "errors"
"fmt"
+ "io"
+ "time"
"github.com/cesanta/docker_auth/auth_server/mgo_session"
"github.com/golang/glog"
@@ -80,6 +83,20 @@ func NewMongoAuth(c *MongoAuthConfig) (*MongoAuth, error) {
}
func (mauth *MongoAuth) Authenticate(account string, password PasswordString) (bool, error) {
+ for true {
+ result, err := mauth.authenticate(account, password)
+ if err == io.EOF {
+ glog.Warningf("EOF error received from Mongo. Retrying connection")
+ time.Sleep(time.Second)
+ continue
+ }
+ return result, err
+ }
+
+ return false, errors.New("Unable to communicate with Mongo.")
+}
+
+func (mauth *MongoAuth) authenticate(account string, password PasswordString) (bool, error) {
// Copy our session
tmp_session := mauth.session.Copy()
// Close up when we are done
diff --git a/auth_server/authz/acl_mongo.go b/auth_server/authz/acl_mongo.go
index 0f2181f3..b9194662 100644
--- a/auth_server/authz/acl_mongo.go
+++ b/auth_server/authz/acl_mongo.go
@@ -3,13 +3,13 @@ package authz
import (
"errors"
"fmt"
- "sync"
- "time"
-
"github.com/cesanta/docker_auth/auth_server/mgo_session"
"github.com/golang/glog"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
+ "io"
+ "sync"
+ "time"
)
type MongoACL []MongoACLEntry
@@ -115,13 +115,20 @@ func (ma *aclMongoAuthorizer) continuouslyUpdateACLCache() {
aclAge := time.Now().Sub(ma.lastCacheUpdate)
glog.V(2).Infof("Updating ACL at %s (ACL age: %s. CacheTTL: %s)", tick, aclAge, ma.config.CacheTTL)
- err := ma.updateACLCache()
- if err == nil {
- continue
+ for true {
+ err := ma.updateACLCache()
+ if err == nil {
+ break
+ } else if err == io.EOF {
+ glog.Warningf("EOF error received from Mongo. Retrying connection")
+ time.Sleep(time.Second)
+ continue
+ } else {
+ glog.Errorf("Failed to update ACL. ERROR: %s", err)
+ glog.Warningf("Using stale ACL (Age: %s, TTL: %s)", aclAge, ma.config.CacheTTL)
+ break
+ }
}
-
- glog.Errorf("Failed to update ACL. ERROR: %s", err)
- glog.Warningf("Using stale ACL (Age: %s, TTL: %s)", aclAge, ma.config.CacheTTL)
}
}
From 79ffb841bcabb5d5fee55b5dd335b8cc13c2fb77 Mon Sep 17 00:00:00 2001
From: dpetzel
Date: Fri, 25 Mar 2016 13:13:09 -0400
Subject: [PATCH 019/209] example acl for catalog
---
examples/reference.yml | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/examples/reference.yml b/examples/reference.yml
index ee0a8cd5..7ef2beaa 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -100,8 +100,7 @@ mongo_auth:
# and a ticket will be issued only for those of the requested actions that are
# allowed by the rule.
# * It is possible to match on user's name ("account"), subject type ("type")
-# and name ("name"; for type=repository which, at the timeof writing, is the
-# only known subject type, this is the image name).
+# and name ("name"; for type=repository this is the image name).
# * Matches are evaluated as shell file name patterns ("globs") by default,
# so "foobar", "f??bar", "f*bar" are all valid. For even more flexibility
# match patterns can be evaluated as regexes by enclosing them in //, e.g.
@@ -144,6 +143,9 @@ acl:
- match: {account: "/.+/", name: "${account}/*"}
actions: ["*"]
comment: "Logged in users have full access to images that are in their 'namespace'"
+ - match: {account: "/.+/", type: "registry", name: "catalog"}
+ actions: ["*"]
+ comment: "Logged in users can query the catalog."
- match: {account: "/.+/"}
actions: ["pull"]
comment: "Logged in users can pull all images."
From 145b4ce20991788b7eb3940e47d396328b82cc27 Mon Sep 17 00:00:00 2001
From: Carson Anderson
Date: Wed, 30 Mar 2016 20:07:27 -0600
Subject: [PATCH 020/209] use correct username key for mongo dial
---
examples/reference.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/examples/reference.yml b/examples/reference.yml
index 7ef2beaa..342b71a8 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -84,7 +84,7 @@ mongo_auth:
# Database name that will be used on the MongoDB server.
database: "docker_auth"
# The username with which to connect to the MongoDB server.
- user: ""
+ username: ""
# Path to the text file with the password in it.
password_file: ""
# Name of the collection in which ACLs will be stored in MongoDB.
@@ -168,7 +168,7 @@ acl_mongo:
# Database name that will be used on the MongoDB server.
database: "docker_auth"
# The username with which to connect to the MongoDB server.
- user: ""
+ username: ""
# Path to the text file with the password in it.
password_file: ""
# Name of the collection in which ACLs will be stored in MongoDB.
From 6f7724277a38b07142fc7cdd3a711eb006539bfc Mon Sep 17 00:00:00 2001
From: Carson Anderson
Date: Mon, 4 Apr 2016 13:16:53 -0600
Subject: [PATCH 021/209] update ldap docs.
---
examples/ldap_auth.yml | 6 ++++++
examples/reference.yml | 15 +++++++++++----
2 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/examples/ldap_auth.yml b/examples/ldap_auth.yml
index 3cc88ff8..bc767159 100644
--- a/examples/ldap_auth.yml
+++ b/examples/ldap_auth.yml
@@ -6,14 +6,20 @@ token:
issuer: Acme auth server
expiration: 900
ldap_auth:
+ # Addr is the hostname:port or ip:port
addr: ldap.example.com:636
# Setup tls connection method to be
# "" or "none": the communication won't be encrypted
# "always": setup LDAP over SSL/TLS
# "starttls": sets StartTLS as the encryption method
tls: always
+ # set to true to allow insecure tls
+ insecure_tls_skip_verify: false
+ # In case bind DN and password is required for querying user information,
+ # specify them here. Plain text password is read from the file.
bind_dn:
bind_password_file:
+ # User query settings. ${account} is expanded from auth request
base: o=example.com
filter: (&(uid=${account})(objectClass=person))
acl:
diff --git a/examples/reference.yml b/examples/reference.yml
index 342b71a8..25abb037 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -61,15 +61,22 @@ google_auth:
# Authentication is performed by first binding to the server, looking up the user entry
# by using the specified filter, and then re-binding using the matched DN and the password provided.
ldap_auth:
- addr: "ldap.example.com:389"
- tls: true
+ # Addr is the hostname:port or ip:port
+ addr: ldap.example.com:636
+ # Setup tls connection method to be
+ # "" or "none": the communication won't be encrypted
+ # "always": setup LDAP over SSL/TLS
+ # "starttls": sets StartTLS as the encryption method
+ tls: always
+ # set to true to allow insecure tls
+ insecure_tls_skip_verify: false
# In case bind DN and password is required for querying user information,
# specify them here. Plain text password is read from the file.
bind_dn:
bind_password_file:
# User query settings. ${account} is expanded from auth request
- base: "o=example.com"
- filter: "(&(uid=${account})(objectClass=person))"
+ base: o=example.com
+ filter: (&(uid=${account})(objectClass=person))
mongo_auth:
# Essentially all options are described here: https://godoc.org/gopkg.in/mgo.v2#DialInfo
From 3b097dc9a9462b65822d9763044fb3c06585ed36 Mon Sep 17 00:00:00 2001
From: rojer
Date: Wed, 6 Apr 2016 14:15:36 +0100
Subject: [PATCH 022/209] Add server.real_ip_header
Closes #83
---
auth_server/server/config.go | 3 ++-
auth_server/server/server.go | 34 +++++++++++++++++++++-------------
examples/reference.yml | 4 ++++
3 files changed, 27 insertions(+), 14 deletions(-)
diff --git a/auth_server/server/config.go b/auth_server/server/config.go
index 1a0addc5..4fe09a61 100644
--- a/auth_server/server/config.go
+++ b/auth_server/server/config.go
@@ -37,12 +37,13 @@ type Config struct {
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"`
+ ACL authz.ACL `yaml:"acl,omitempty"`
ACLMongo *authz.ACLMongoConfig `yaml:"acl_mongo,omitempty"`
}
type ServerConfig struct {
ListenAddress string `yaml:"addr,omitempty"`
+ RealIPHeader string `yaml:"real_ip_header,omitempty"`
CertFile string `yaml:"certificate,omitempty"`
KeyFile string `yaml:"key,omitempty"`
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index c96ae41b..3e8837e1 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -88,13 +88,14 @@ func NewAuthServer(c *Config) (*AuthServer, error) {
}
type authRequest struct {
- RemoteAddr string
- RemoteIP net.IP
- User string
- Password authn.PasswordString
- Account string
- Service string
- Scopes []authScope
+ RemoteConnAddr string
+ RemoteAddr string
+ RemoteIP net.IP
+ User string
+ Password authn.PasswordString
+ Account string
+ Service string
+ Scopes []authScope
}
type authScope struct {
@@ -114,10 +115,9 @@ func (ar authRequest) String() string {
func parseRemoteAddr(ra string) net.IP {
colonIndex := strings.LastIndex(ra, ":")
- if colonIndex == -1 {
- return nil
+ if colonIndex > 0 && ra[colonIndex-1] >= 0x30 && ra[colonIndex-1] <= 0x39 {
+ ra = ra[:colonIndex]
}
- ra = ra[:colonIndex]
if ra[0] == '[' && ra[len(ra)-1] == ']' { // IPv6
ra = ra[1 : len(ra)-1]
}
@@ -126,10 +126,18 @@ func parseRemoteAddr(ra string) net.IP {
}
func (as *AuthServer) ParseRequest(req *http.Request) (*authRequest, error) {
- ar := &authRequest{RemoteAddr: req.RemoteAddr}
- ar.RemoteIP = parseRemoteAddr(req.RemoteAddr)
+ ar := &authRequest{RemoteConnAddr: req.RemoteAddr, RemoteAddr: req.RemoteAddr}
+ if as.config.Server.RealIPHeader != "" {
+ hv := req.Header.Get(as.config.Server.RealIPHeader)
+ ar.RemoteAddr = strings.TrimSpace(strings.Split(hv, ",")[0])
+ glog.V(3).Infof("Conn ip %s, %s: %s, addr: %s", ar.RemoteAddr, as.config.Server.RealIPHeader, hv, ar.RemoteAddr)
+ if ar.RemoteAddr == "" {
+ return nil, fmt.Errorf("client address not provided")
+ }
+ }
+ ar.RemoteIP = parseRemoteAddr(ar.RemoteAddr)
if ar.RemoteIP == nil {
- return nil, fmt.Errorf("unable to parse remote addr %s", req.RemoteAddr)
+ return nil, fmt.Errorf("unable to parse remote addr %s", ar.RemoteAddr)
}
user, password, haveBasicAuth := req.BasicAuth()
if haveBasicAuth {
diff --git a/examples/reference.yml b/examples/reference.yml
index 25abb037..bd1dba0f 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -15,6 +15,10 @@ server: # Server settings.
# TLS certificate and key.
certificate: "/path/to/server.pem"
key: "/path/to/server.key"
+ # Take client's address from the specified HTTP header instead of connection.
+ # May be useful if the server is behind a proxy or load balancer.
+ # If configured, this header must be present, requests without it will be rejected.
+ # real_ip_header: "X-Forwarded-For"
token: # Settings for the tokens.
issuer: "Acme auth server" # Must match issuer in the Registry config.
From 13128a9ed5ecc086ba52ab667d2e7a00f069dc76 Mon Sep 17 00:00:00 2001
From: rojer
Date: Mon, 6 Jun 2016 21:17:03 +0100
Subject: [PATCH 023/209] Mention reference.yml in ldap auth example
Closes #102
---
examples/ldap_auth.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/examples/ldap_auth.yml b/examples/ldap_auth.yml
index bc767159..bb3f46e1 100644
--- a/examples/ldap_auth.yml
+++ b/examples/ldap_auth.yml
@@ -1,3 +1,6 @@
+# LDAP server authentication example.
+# See reference.yml for additional options.
+
server:
addr: :5001
certificate: /path/to/server.pem
From 849152fa26a67bb32567171af96afc93d1cf5119 Mon Sep 17 00:00:00 2001
From: rojer
Date: Mon, 6 Jun 2016 23:03:55 +0100
Subject: [PATCH 024/209] Handle "invalid credentials" and "no such user" as
non-errors
It's kind of expected, we should return 403 in this case, not 500.
---
auth_server/authn/ldap_auth.go | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/auth_server/authn/ldap_auth.go b/auth_server/authn/ldap_auth.go
index 4f849f78..e2fc2248 100755
--- a/auth_server/authn/ldap_auth.go
+++ b/auth_server/authn/ldap_auth.go
@@ -75,10 +75,16 @@ func (la *LDAPAuth) Authenticate(account string, password PasswordString) (bool,
if uSearchErr != nil {
return false, uSearchErr
}
+ if accountEntryDN == "" {
+ return false, nil // User does not exist
+ }
// Bind as the user to verify their password
if len(accountEntryDN) > 0 {
err := l.Bind(accountEntryDN, string(password))
if err != nil {
+ if ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials) {
+ return false, nil
+ }
return false, err
}
}
@@ -173,8 +179,10 @@ func (la *LDAPAuth) ldapSearch(l *ldap.Conn, baseDN *string, filter *string, att
return "", err
}
- if len(sr.Entries) != 1 {
- return "", fmt.Errorf("User does not exist or too many entries returned.")
+ if len(sr.Entries) == 0 {
+ return "", nil // User does not exist
+ } else if len(sr.Entries) > 1 {
+ return "", fmt.Errorf("Too many entries returned.")
}
var buffer bytes.Buffer
From 47020de65ae7985c828afd24fca9ffbf429855e8 Mon Sep 17 00:00:00 2001
From: rojer
Date: Mon, 20 Jun 2016 10:54:02 +0100
Subject: [PATCH 025/209] External authentication
---
README.md | 1 +
auth_server/authn/ext_auth.go | 113 +++++++++++++++++++++++++++++++++
auth_server/authn/ldap_auth.go | 0
auth_server/server/config.go | 8 ++-
auth_server/server/server.go | 3 +
examples/reference.yml | 7 ++
6 files changed, 131 insertions(+), 1 deletion(-)
create mode 100644 auth_server/authn/ext_auth.go
mode change 100755 => 100644 auth_server/authn/ldap_auth.go
diff --git a/README.md b/README.md
index faca98f8..47786c66 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ Supported authentication methods:
* 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
+ * External program
Supported authorization methods:
* Static ACL
diff --git a/auth_server/authn/ext_auth.go b/auth_server/authn/ext_auth.go
new file mode 100644
index 00000000..d7a85a73
--- /dev/null
+++ b/auth_server/authn/ext_auth.go
@@ -0,0 +1,113 @@
+/*
+ Copyright 2016 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 (
+ "encoding/json"
+ "fmt"
+ "os/exec"
+ "strings"
+ "syscall"
+
+ "github.com/golang/glog"
+)
+
+type ExtAuthConfig struct {
+ Command string `yaml:"command"`
+ Args []string `yaml:"args"`
+}
+
+type ExtAuthRequest struct {
+ User string `json:"user"`
+ Password string `json:"password"`
+}
+
+type ExtAuthStatus int
+
+const (
+ ExtAuthAllowed ExtAuthStatus = 0
+ ExtAuthDenied ExtAuthStatus = 1
+ ExtAuthNoMatch ExtAuthStatus = 2
+ ExtAuthError ExtAuthStatus = 3
+)
+
+type ExtAuthResponse struct {
+ Status int `json:"status"`
+ Message string `json:"message,omitempty"`
+}
+
+func (c *ExtAuthConfig) Validate() error {
+ if c.Command == "" {
+ return fmt.Errorf("command is not set")
+ }
+ if _, err := exec.LookPath(c.Command); err != nil {
+ return fmt.Errorf("invalid command %q: %s", c.Command, err)
+ }
+ return nil
+}
+
+type extAuth struct {
+ cfg *ExtAuthConfig
+}
+
+func (r ExtAuthRequest) String() string {
+ if r.Password != "" {
+ r.Password = "***"
+ }
+ b, _ := json.Marshal(r)
+ return string(b)
+}
+
+func NewExtAuth(cfg *ExtAuthConfig) *extAuth {
+ glog.Infof("External authenticator: %s %s", cfg.Command, strings.Join(cfg.Args, " "))
+ return &extAuth{cfg: cfg}
+}
+
+func (ea *extAuth) Authenticate(user string, password PasswordString) (bool, error) {
+ cmd := exec.Command(ea.cfg.Command, ea.cfg.Args...)
+ cmd.Stdin = strings.NewReader(fmt.Sprintf("%s %s", user, string(password)))
+ _, err := cmd.Output()
+ es := 0
+ et := ""
+ if err == nil {
+ } else if ee, ok := err.(*exec.ExitError); ok {
+ es = ee.Sys().(syscall.WaitStatus).ExitStatus()
+ et = string(ee.Stderr)
+ } else {
+ es = int(ExtAuthError)
+ et = fmt.Sprintf("cmd run error: %s", err)
+ }
+ glog.V(2).Infof("%s %s -> %d", cmd.Path, cmd.Args, es)
+ switch ExtAuthStatus(es) {
+ case ExtAuthAllowed:
+ return true, nil
+ case ExtAuthDenied:
+ return false, nil
+ case ExtAuthNoMatch:
+ return false, NoMatch
+ default:
+ glog.Errorf("Ext command error: %d %s", es, et)
+ }
+ return false, fmt.Errorf("bad return code from command: %d", es)
+}
+
+func (sua *extAuth) Stop() {
+}
+
+func (sua *extAuth) Name() string {
+ return "external"
+}
diff --git a/auth_server/authn/ldap_auth.go b/auth_server/authn/ldap_auth.go
old mode 100755
new mode 100644
diff --git a/auth_server/server/config.go b/auth_server/server/config.go
index 4fe09a61..4c547c15 100644
--- a/auth_server/server/config.go
+++ b/auth_server/server/config.go
@@ -37,6 +37,7 @@ type Config struct {
GoogleAuth *authn.GoogleAuthConfig `yaml:"google_auth,omitempty"`
LDAPAuth *authn.LDAPAuthConfig `yaml:"ldap_auth,omitempty"`
MongoAuth *authn.MongoAuthConfig `yaml:"mongo_auth,omitempty"`
+ ExtAuth *authn.ExtAuthConfig `yaml:"ext_auth,omitempty"`
ACL authz.ACL `yaml:"acl,omitempty"`
ACLMongo *authz.ACLMongoConfig `yaml:"acl_mongo,omitempty"`
}
@@ -72,7 +73,7 @@ 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 && c.MongoAuth == nil {
+ if c.Users == nil && c.ExtAuth == 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 {
@@ -95,6 +96,11 @@ func validate(c *Config) error {
gac.HTTPTimeout = 10
}
}
+ if c.ExtAuth != nil {
+ if err := c.ExtAuth.Validate(); err != nil {
+ return fmt.Errorf("bad ext_auth config: %s", err)
+ }
+ }
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")
}
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 3e8837e1..a4ddea7e 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -62,6 +62,9 @@ func NewAuthServer(c *Config) (*AuthServer, error) {
if c.Users != nil {
as.authenticators = append(as.authenticators, authn.NewStaticUserAuth(c.Users))
}
+ if c.ExtAuth != nil {
+ as.authenticators = append(as.authenticators, authn.NewExtAuth(c.ExtAuth))
+ }
if c.GoogleAuth != nil {
ga, err := authn.NewGoogleAuth(c.GoogleAuth)
if err != nil {
diff --git a/examples/reference.yml b/examples/reference.yml
index bd1dba0f..6216c627 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -103,6 +103,13 @@ mongo_auth:
# Unlike acl_mongo we don't cache the full user set. We just query mongo for
# an exact match for each authorization
+# External authentication - call an external progam to authenticate user.
+# Username and password are passed to command's stdin and exit code is examined.
+# 0 - allow, 1 - deny, 2 - no match, other - error.
+ext_auth:
+ command: "/usr/local/bin/my_auth" # Can be a relative path too; $PATH works.
+ args: ["--flag", "--more", "--flags"]
+
# Authorization methods. All are tried, any one returning success is sufficient.
# At least one must be configured.
From 986e23f62419759b1a0b393a55d9b4836529d949 Mon Sep 17 00:00:00 2001
From: rojer
Date: Fri, 1 Jul 2016 12:16:19 +0100
Subject: [PATCH 026/209] Version-tag builds
---
auth_server/.gitignore | 1 +
auth_server/Makefile | 12 ++++++--
auth_server/gen_version.py | 60 ++++++++++++++++++++++++++++++++++++++
auth_server/main.go | 4 +++
4 files changed, 75 insertions(+), 2 deletions(-)
create mode 100755 auth_server/gen_version.py
diff --git a/auth_server/.gitignore b/auth_server/.gitignore
index 42f4739b..ea8e804c 100644
--- a/auth_server/.gitignore
+++ b/auth_server/.gitignore
@@ -1,3 +1,4 @@
ca-certificates.crt
auth_server
Godeps/
+version.*
diff --git a/auth_server/Makefile b/auth_server/Makefile
index 39b480f7..3338c5c1 100644
--- a/auth_server/Makefile
+++ b/auth_server/Makefile
@@ -2,6 +2,7 @@ MAKEFLAGS += --warn-undefined-variables
IMAGE ?= cesanta/docker_auth
COMPRESS_BINARY ?= false
CA_BUNDLE = /etc/ssl/certs/ca-certificates.crt
+VERSION = $(shell cat version.txt)
BUILDER_IMAGE ?= centurylink/golang-builder
BUILDER_IMAGE_EXTRA-build-cross = -cross
@@ -15,12 +16,12 @@ local: build-local
update-deps:
go get -v -u -f github.com/tools/godep github.com/jteeuwen/go-bindata/...
+ go generate ./...
godep:
godep save
build-local: update-deps
- go generate ./...
go build
ca-certificates.crt:
@@ -28,11 +29,18 @@ ca-certificates.crt:
docker-build:
docker run --rm -v $(PWD):/src -e COMPRESS_BINARY=$(COMPRESS_BINARY) $(BUILDER_OPTS-$@) $(BUILDER_IMAGE)$(BUILDER_IMAGE_EXTRA-$@) $(IMAGE)
+ @echo === Built version $(VERSION) ===
build build-cross: update-deps godep ca-certificates.crt docker-build
+docker-tag:
+ docker tag $(IMAGE):latest $(IMAGE):$(VERSION)
+
docker-tag-%:
- docker tag -f $(IMAGE):latest $(IMAGE):$*
+ docker tag $(IMAGE):latest $(IMAGE):$*
+
+docker-push:
+ docker push $(IMAGE):latest $(IMAGE):$(VERSION)
docker-push-%: docker-tag-%
docker push $(IMAGE):$*
diff --git a/auth_server/gen_version.py b/auth_server/gen_version.py
new file mode 100755
index 00000000..79132cd7
--- /dev/null
+++ b/auth_server/gen_version.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+
+import datetime
+import sys
+
+# Debian/Ubuntu: apt-get install python-git
+# PIP: pip install GitPython
+import git
+
+repo = git.Repo('.', search_parent_directories=True)
+
+
+def get_tag_for_commit(repo, commit):
+ for tag in repo.tags:
+ if tag.commit == commit:
+ return tag.name
+ return None
+
+
+if repo.head.is_detached:
+ branch_or_tag = get_tag_for_commit(repo, repo.head.commit)
+ if branch_or_tag is None:
+ branch_or_tag = '?'
+else:
+ branch_or_tag = repo.active_branch
+
+dirty = repo.is_dirty()
+
+ts = datetime.datetime.utcnow()
+build_id = '%s/%s@%s%s' % (ts.strftime('%Y%m%d-%H%M%S'),
+ branch_or_tag,
+ str(repo.head.commit)[:8],
+ '+' if dirty else '')
+
+version = None
+if not dirty:
+ version = get_tag_for_commit(repo, repo.head.commit)
+if version is None:
+ version = ts.strftime('%Y%m%d%H%M%S')
+
+
+if len(sys.argv) == 1 or sys.argv[1] == '-':
+ f = sys.stdout
+else:
+ f = open(sys.argv[1], 'w')
+
+with open('version.go', 'w') as f:
+ f.write("""\
+package main
+
+const (
+\tVersion = "{version}"
+\tBuildId = "{build_id}"
+)
+""".format(version=version, build_id=build_id))
+
+with open('version.txt', 'w') as f:
+ f.write(version)
+
+f.close()
diff --git a/auth_server/main.go b/auth_server/main.go
index 086393ed..caded573 100644
--- a/auth_server/main.go
+++ b/auth_server/main.go
@@ -14,6 +14,8 @@
limitations under the License.
*/
+//go:generate ./gen_version.py
+
package main // import "github.com/cesanta/docker_auth/auth_server"
import (
@@ -158,6 +160,8 @@ func main() {
rand.Seed(time.Now().UnixNano())
glog.CopyStandardLogTo("INFO")
+ glog.Infof("docker_auth %s build %s", Version, BuildId)
+
cf := flag.Arg(0)
if cf == "" {
glog.Exitf("Config file not specified")
From 334610c3ec9ee153eb81ad5e45b05c0b7cfa09e5 Mon Sep 17 00:00:00 2001
From: rojer
Date: Fri, 1 Jul 2016 12:38:09 +0100
Subject: [PATCH 027/209] Fix docker-push target
---
auth_server/Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/auth_server/Makefile b/auth_server/Makefile
index 3338c5c1..879a7c12 100644
--- a/auth_server/Makefile
+++ b/auth_server/Makefile
@@ -40,7 +40,7 @@ docker-tag-%:
docker tag $(IMAGE):latest $(IMAGE):$*
docker-push:
- docker push $(IMAGE):latest $(IMAGE):$(VERSION)
+ docker push $(IMAGE):$(VERSION)
docker-push-%: docker-tag-%
docker push $(IMAGE):$*
From f3244468e3e0be68cbbcb566f84e5a2efd6ef27e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Pinson?=
Date: Wed, 13 Jul 2016 18:22:25 +0200
Subject: [PATCH 028/209] Return NoMatch when user does not exist in LDAP
---
auth_server/authn/ldap_auth.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/auth_server/authn/ldap_auth.go b/auth_server/authn/ldap_auth.go
index e2fc2248..769a78dc 100644
--- a/auth_server/authn/ldap_auth.go
+++ b/auth_server/authn/ldap_auth.go
@@ -76,7 +76,7 @@ func (la *LDAPAuth) Authenticate(account string, password PasswordString) (bool,
return false, uSearchErr
}
if accountEntryDN == "" {
- return false, nil // User does not exist
+ return false, NoMatch // User does not exist
}
// Bind as the user to verify their password
if len(accountEntryDN) > 0 {
From a6b51a80da2dc15b2a52647d69b45b70c9ca0b1e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Pinson?=
Date: Wed, 13 Jul 2016 15:49:27 +0200
Subject: [PATCH 029/209] Add GitHub authn provider
Add a TokenDB struct and refactor Google authn provider
---
auth_server/authn/authn.go | 2 +
auth_server/authn/data/github_auth.tmpl | 6 +
auth_server/authn/github_auth.go | 258 ++++++++++++++++++++++++
auth_server/authn/google_auth.go | 105 ++--------
auth_server/authn/tokendb.go | 147 ++++++++++++++
auth_server/server/config.go | 23 ++-
auth_server/server/server.go | 19 +-
examples/reference.yml | 21 ++
8 files changed, 492 insertions(+), 89 deletions(-)
create mode 100644 auth_server/authn/data/github_auth.tmpl
create mode 100644 auth_server/authn/github_auth.go
create mode 100644 auth_server/authn/tokendb.go
diff --git a/auth_server/authn/authn.go b/auth_server/authn/authn.go
index 5f5a8b56..b7af6531 100644
--- a/auth_server/authn/authn.go
+++ b/auth_server/authn/authn.go
@@ -24,6 +24,7 @@ type Authenticator interface {
// 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.
+ // Another special WrongPass error is returned if the authorizer failed to authenticate.
// Implementations must be goroutine-safe.
Authenticate(user string, password PasswordString) (bool, error)
@@ -37,6 +38,7 @@ type Authenticator interface {
}
var NoMatch = errors.New("did not match any rule")
+var WrongPass = errors.New("wrong password for user")
//go:generate go-bindata -pkg authn -modtime 1 -mode 420 data/
diff --git a/auth_server/authn/data/github_auth.tmpl b/auth_server/authn/data/github_auth.tmpl
new file mode 100644
index 00000000..9fb86871
--- /dev/null
+++ b/auth_server/authn/data/github_auth.tmpl
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/auth_server/authn/github_auth.go b/auth_server/authn/github_auth.go
new file mode 100644
index 00000000..67540561
--- /dev/null
+++ b/auth_server/authn/github_auth.go
@@ -0,0 +1,258 @@
+/*
+ Copyright 2016 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 (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "html/template"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/golang/glog"
+)
+
+type GitHubAuthConfig struct {
+ Organization string `yaml:"organization,omitempty"`
+ ClientId string `yaml:"client_id,omitempty"`
+ ClientSecret string `yaml:"client_secret,omitempty"`
+ ClientSecretFile string `yaml:"client_secret_file,omitempty"`
+ TokenDB string `yaml:"token_db,omitempty"`
+ HTTPTimeout time.Duration `yaml:"http_timeout,omitempty"`
+ RevalidateAfter time.Duration `yaml:"revalidate_after,omitempty"`
+}
+
+type GitHubAuthRequest struct {
+ Action string `json:"action,omitempty"`
+ Code string `json:"code,omitempty"`
+ Token string `json:"token,omitempty"`
+}
+
+type GitHubTokenUser struct {
+ Login string `json:"login,omitempty"`
+ Email string `json:"email,omitempty"`
+}
+
+type GitHubAuth struct {
+ config *GitHubAuthConfig
+ db TokenDB
+ client *http.Client
+ tmpl *template.Template
+}
+
+func NewGitHubAuth(c *GitHubAuthConfig) (*GitHubAuth, error) {
+ db, err := NewTokenDB(c.TokenDB)
+ if err != nil {
+ return nil, err
+ }
+ glog.Infof("GitHub auth token DB at %s", c.TokenDB)
+ return &GitHubAuth{
+ config: c,
+ db: db,
+ client: &http.Client{Timeout: 10 * time.Second},
+ tmpl: template.Must(template.New("github_auth").Parse(string(MustAsset("data/github_auth.tmpl")))),
+ }, nil
+}
+
+func (gha *GitHubAuth) doGitHubAuthPage(rw http.ResponseWriter, req *http.Request) {
+ if err := gha.tmpl.Execute(rw, struct{ ClientId string }{ClientId: gha.config.ClientId}); err != nil {
+ http.Error(rw, fmt.Sprintf("Template error: %s", err), http.StatusInternalServerError)
+ }
+}
+
+func (gha *GitHubAuth) DoGitHubAuth(rw http.ResponseWriter, req *http.Request) {
+ code := req.URL.Query().Get("code")
+
+ if code != "" {
+ gha.doGitHubAuthCreateToken(rw, code)
+ } else if req.Method == "GET" {
+ gha.doGitHubAuthPage(rw, req)
+ return
+ }
+}
+
+func (gha *GitHubAuth) doGitHubAuthCreateToken(rw http.ResponseWriter, code string) {
+ data := url.Values{
+ "code": []string{string(code)},
+ "client_id": []string{gha.config.ClientId},
+ "client_secret": []string{gha.config.ClientSecret},
+ }
+ req, err := http.NewRequest("POST", "https://github.com/login/oauth/access_token", bytes.NewBufferString(data.Encode()))
+ if err != nil {
+ http.Error(rw, fmt.Sprintf("Error creating request to GitHub auth backend: %s", err), http.StatusServiceUnavailable)
+ return
+ }
+ req.Header.Add("Accept", "application/json")
+
+ resp, err := gha.client.Do(req)
+ if err != nil {
+ http.Error(rw, fmt.Sprintf("Error talking to GitHub auth backend: %s", err), http.StatusServiceUnavailable)
+ return
+ }
+ codeResp, _ := ioutil.ReadAll(resp.Body)
+ resp.Body.Close()
+ glog.V(2).Infof("Code to token resp: %s", strings.Replace(string(codeResp), "\n", " ", -1))
+
+ var c2t CodeToTokenResponse
+ err = json.Unmarshal(codeResp, &c2t)
+ if err != nil || c2t.Error != "" || c2t.ErrorDescription != "" {
+ var et string
+ if err != nil {
+ et = err.Error()
+ } else {
+ et = fmt.Sprintf("%s: %s", c2t.Error, c2t.ErrorDescription)
+ }
+ http.Error(rw, fmt.Sprintf("Failed to get token: %s", et), http.StatusBadRequest)
+ return
+ }
+
+ user, err := gha.validateAccessToken(c2t.AccessToken)
+ if err != nil {
+ glog.Errorf("Newly-acquired token is invalid: %+v %s", c2t, err)
+ http.Error(rw, "Newly-acquired token is invalid", http.StatusInternalServerError)
+ return
+ }
+
+ glog.Infof("New GitHub auth token for %s", user)
+
+ v := &TokenDBValue{
+ TokenType: c2t.TokenType,
+ AccessToken: c2t.AccessToken,
+ ValidUntil: time.Now().Add(gha.config.RevalidateAfter),
+ }
+ dp, err := gha.db.StoreToken(user, v, true)
+ if err != nil {
+ glog.Errorf("Failed to record server token: %s", err)
+ http.Error(rw, "Failed to record server token: %s", http.StatusInternalServerError)
+ return
+ }
+
+ fmt.Fprintf(rw, `Server logged in; now run "docker login", use %s as login and %s as password.`, user, dp)
+}
+
+func (gha *GitHubAuth) validateAccessToken(token string) (user string, err error) {
+ req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
+ if err != nil {
+ err = fmt.Errorf("could not create request to get information for token %s: %s", token, err)
+ return
+ }
+ req.Header.Add("Authorization", fmt.Sprintf("token %s", token))
+ req.Header.Add("Accept", "application/json")
+
+ resp, err := gha.client.Do(req)
+ if err != nil {
+ err = fmt.Errorf("could not verify token %s: %s", token, err)
+ return
+ }
+ body, _ := ioutil.ReadAll(resp.Body)
+ resp.Body.Close()
+
+ var ti GitHubTokenUser
+ err = json.Unmarshal(body, &ti)
+ if err != nil {
+ err = fmt.Errorf("could not unmarshal token user info %q: %s", string(body), err)
+ return
+ }
+ glog.V(2).Infof("Token user info: %+v", strings.Replace(string(body), "\n", " ", -1))
+
+ err = gha.checkOrganization(token, ti.Login)
+ if err != nil {
+ err = fmt.Errorf("could not validate organization: %s", err)
+ return
+ }
+
+ return ti.Login, nil
+}
+
+func (gha *GitHubAuth) checkOrganization(token, user string) (err error) {
+ if gha.config.Organization == "" {
+ return nil
+ }
+ url := fmt.Sprintf("https://api.github.com/orgs/%s/members/%s", gha.config.Organization, user)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ err = fmt.Errorf("could not create request to get organization membership: %s", err)
+ return
+ }
+ req.Header.Add("Authorization", fmt.Sprintf("token %s", token))
+
+ resp, err := gha.client.Do(req)
+ if err != nil {
+ return
+ }
+
+ switch resp.StatusCode {
+ case http.StatusNoContent:
+ return nil
+ case http.StatusNotFound:
+ return fmt.Errorf("%s is not a member of organization %s", user, gha.config.Organization)
+ case http.StatusFound:
+ return fmt.Errorf("token %s could not get membership for organization %s", token, gha.config.Organization)
+ }
+
+ return fmt.Errorf("Unknown status for membership of organization %s: %s", gha.config.Organization, resp.Status)
+}
+
+func (gha *GitHubAuth) validateServerToken(user string) (*TokenDBValue, error) {
+ v, err := gha.db.GetValue(user)
+ if err != nil || v == nil {
+ if err == nil {
+ err = errors.New("no db value, please sign out and sign in again.")
+ }
+ return nil, err
+ }
+ tokenUser, err := gha.validateAccessToken(v.AccessToken)
+ if err != nil {
+ glog.Warningf("Token for %q failed validation: %s", user, err)
+ return nil, fmt.Errorf("server token invalid: %s", err)
+ }
+ if tokenUser != user {
+ glog.Errorf("token for wrong user: expected %s, found %s", user, tokenUser)
+ return nil, fmt.Errorf("found token for wrong user")
+ }
+ v.ValidUntil = time.Now().Add(gha.config.RevalidateAfter)
+ texp := v.ValidUntil.Sub(time.Now())
+ glog.V(1).Infof("Validated GitHub auth token for %s (exp %d)", user, int(texp.Seconds()))
+ return v, nil
+}
+
+func (gha *GitHubAuth) Authenticate(user string, password PasswordString) (bool, error) {
+ err := gha.db.ValidateToken(user, password)
+ if err == ExpiredToken {
+ _, err = gha.validateServerToken(user)
+ if err != nil {
+ return false, err
+ }
+ } else if err != nil {
+ return false, err
+ }
+ return true, nil
+}
+
+func (gha *GitHubAuth) Stop() {
+ gha.db.Close()
+ glog.Info("Token DB closed")
+}
+
+func (gha *GitHubAuth) Name() string {
+ return "GitHub"
+}
diff --git a/auth_server/authn/google_auth.go b/auth_server/authn/google_auth.go
index efd005ec..c0da268e 100644
--- a/auth_server/authn/google_auth.go
+++ b/auth_server/authn/google_auth.go
@@ -27,10 +27,7 @@ import (
"strings"
"time"
- "github.com/dchest/uniuri"
"github.com/golang/glog"
- "github.com/syndtr/goleveldb/leveldb"
- "golang.org/x/crypto/bcrypt"
)
type GoogleAuthConfig struct {
@@ -120,31 +117,15 @@ type ProfileResponse struct {
// There are more fields, but we only need email.
}
-// Database-related stuff.
-const (
- tokenDBPrefix = "t:" // Keys in the database are t:email@example.com
-)
-
-// TokenDBValue is stored in the database, JSON-serialized.
-type TokenDBValue struct {
- TokenType string `json:"token_type,omitempty"` // Usually "Bearer"
- AccessToken string `json:"access_token,omitempty"`
- RefreshToken string `json:"refresh_token,omitempty"`
- ValidUntil time.Time `json:"valid_until,omitempty"`
- // DockerPassword is the temporary password we use to authenticate Docker users.
- // Gneerated at the time of token creation, stored here as a BCrypt hash.
- DockerPassword string `json:"docker_password,omitempty"`
-}
-
type GoogleAuth struct {
config *GoogleAuthConfig
- db *leveldb.DB
+ db TokenDB
client *http.Client
tmpl *template.Template
}
func NewGoogleAuth(c *GoogleAuthConfig) (*GoogleAuth, error) {
- db, err := leveldb.OpenFile(c.TokenDB, nil)
+ db, err := NewTokenDB(c.TokenDB)
if err != nil {
return nil, err
}
@@ -240,17 +221,13 @@ func (ga *GoogleAuth) doGoogleAuthCreateToken(rw http.ResponseWriter, code strin
glog.Infof("New Google auth token for %s (exp %d)", user, c2t.ExpiresIn)
- dp := uniuri.New()
- dph, _ := bcrypt.GenerateFromPassword([]byte(dp), bcrypt.DefaultCost)
-
v := &TokenDBValue{
- TokenType: c2t.TokenType,
- AccessToken: c2t.AccessToken,
- RefreshToken: c2t.RefreshToken,
- ValidUntil: time.Now().Add(time.Duration(c2t.ExpiresIn-30) * time.Second),
- DockerPassword: string(dph),
+ TokenType: c2t.TokenType,
+ AccessToken: c2t.AccessToken,
+ RefreshToken: c2t.RefreshToken,
+ ValidUntil: time.Now().Add(time.Duration(c2t.ExpiresIn-30) * time.Second),
}
- err = ga.setServerToken(user, v)
+ dp, err := ga.db.StoreToken(user, v, true)
if err != nil {
glog.Errorf("Failed to record server token: %s", err)
http.Error(rw, "Failed to record server token: %s", http.StatusInternalServerError)
@@ -307,10 +284,6 @@ func (ga *GoogleAuth) checkDomain(email string) error {
return nil
}
-func getDBKey(user string) []byte {
- return []byte(fmt.Sprintf("%s%s", tokenDBPrefix, user))
-}
-
// https://developers.google.com/identity/protocols/OAuth2WebServer#refresh
func (ga *GoogleAuth) refreshAccessToken(refreshToken string) (rtr RefreshTokenResponse, err error) {
resp, err := ga.client.PostForm(
@@ -356,26 +329,8 @@ func (ga *GoogleAuth) validateAccessToken(toktype, token string) (user string, e
return pr.Email, nil
}
-func (ga *GoogleAuth) getDBValue(user string) (*TokenDBValue, error) {
- valueStr, err := ga.db.Get(getDBKey(user), nil)
- switch {
- case err == leveldb.ErrNotFound:
- return nil, nil
- case err != nil:
- glog.Errorf("error accessing token db: %s", err)
- return nil, fmt.Errorf("error accessing token db: %s", err)
- }
- var dbv TokenDBValue
- err = json.Unmarshal(valueStr, &dbv)
- if err != nil {
- glog.Errorf("bad DB value for %q (%q): %s", user, string(valueStr), err)
- return nil, fmt.Errorf("bad DB value", err)
- }
- return &dbv, nil
-}
-
func (ga *GoogleAuth) validateServerToken(user string) (*TokenDBValue, error) {
- v, err := ga.getDBValue(user)
+ v, err := ga.db.GetValue(user)
if err != nil || v == nil {
if err == nil {
err = errors.New("no db value, please sign out and sign in again.")
@@ -392,7 +347,7 @@ func (ga *GoogleAuth) validateServerToken(user string) (*TokenDBValue, error) {
v.AccessToken = rtr.AccessToken
v.ValidUntil = time.Now().Add(time.Duration(rtr.ExpiresIn-30) * time.Second)
glog.Infof("Refreshed auth token for %s (exp %d)", user, rtr.ExpiresIn)
- err = ga.setServerToken(user, v)
+ _, err = ga.db.StoreToken(user, v, false)
if err != nil {
glog.Errorf("Failed to record refreshed token: %s", err)
return nil, fmt.Errorf("failed to record refreshed token: %s", err)
@@ -412,26 +367,6 @@ func (ga *GoogleAuth) validateServerToken(user string) (*TokenDBValue, error) {
return v, nil
}
-func (ga *GoogleAuth) setServerToken(user string, v *TokenDBValue) error {
- data, err := json.Marshal(v)
- if err != nil {
- return err
- }
- err = ga.db.Put(getDBKey(user), data, nil)
- if err != nil {
- glog.Errorf("failed to set token data for %s: %s", user, err)
- }
- glog.V(2).Infof("Server tokens for %s: %s", user, string(data))
- return err
-}
-
-func (ga *GoogleAuth) deleteServerToken(user string) {
- glog.V(1).Infof("deleting token for %s", user)
- if err := ga.db.Delete(getDBKey(user), nil); err != nil {
- glog.Errorf("failed to delete %s: %s", user, err)
- }
-}
-
func (ga *GoogleAuth) doGoogleAuthCheck(rw http.ResponseWriter, token string) {
// First, authenticate web user.
ti, err := ga.getIDTokenInfo(token)
@@ -457,26 +392,22 @@ func (ga *GoogleAuth) doGoogleAuthSignOut(rw http.ResponseWriter, token string)
http.Error(rw, fmt.Sprintf("Could not verify user token: %s", err), http.StatusBadRequest)
return
}
- ga.deleteServerToken(ti.Email)
+ err = ga.db.DeleteToken(ti.Email)
+ if err != nil {
+ glog.Error(err)
+ }
fmt.Fprint(rw, "signed out")
}
func (ga *GoogleAuth) Authenticate(user string, password PasswordString) (bool, error) {
- dbv, err := ga.getDBValue(user)
- if err != nil {
- return false, err
- }
- if dbv == nil {
- return false, NoMatch
- }
- if time.Now().After(dbv.ValidUntil) {
- dbv, err = ga.validateServerToken(user)
+ err := ga.db.ValidateToken(user, password)
+ if err == ExpiredToken {
+ _, err = ga.validateServerToken(user)
if err != nil {
return false, err
}
- }
- if bcrypt.CompareHashAndPassword([]byte(dbv.DockerPassword), []byte(password)) != nil {
- return false, nil
+ } else if err != nil {
+ return false, err
}
return true, nil
}
diff --git a/auth_server/authn/tokendb.go b/auth_server/authn/tokendb.go
new file mode 100644
index 00000000..daaec171
--- /dev/null
+++ b/auth_server/authn/tokendb.go
@@ -0,0 +1,147 @@
+/*
+ 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 (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "time"
+
+ "golang.org/x/crypto/bcrypt"
+
+ "github.com/dchest/uniuri"
+ "github.com/golang/glog"
+ "github.com/syndtr/goleveldb/leveldb"
+)
+
+const (
+ tokenDBPrefix = "t:" // Keys in the database are t:email@example.com
+)
+
+var ExpiredToken = errors.New("expired token")
+
+// TokenDB stores tokens using LevelDB
+type TokenDB interface {
+ // GetValue takes a username returns the corresponding token
+ GetValue(string) (*TokenDBValue, error)
+
+ // StoreToken takes a username and token, stores them in the DB
+ // and returns a password and error
+ StoreToken(string, *TokenDBValue, bool) (string, error)
+
+ // ValidateTOken takes a username and password
+ // and returns an error
+ ValidateToken(string, PasswordString) error
+
+ // DeleteToken takes a username
+ // and deletes the corresponding token from the DB
+ DeleteToken(string) error
+
+ // Composed from leveldb.DB
+ Close() error
+}
+
+// TokenDB stores tokens using LevelDB
+type TokenDBImpl struct {
+ *leveldb.DB
+}
+
+// TokenDBValue is stored in the database, JSON-serialized.
+type TokenDBValue struct {
+ TokenType string `json:"token_type,omitempty"` // Usually "Bearer"
+ AccessToken string `json:"access_token,omitempty"`
+ RefreshToken string `json:"refresh_token,omitempty"`
+ ValidUntil time.Time `json:"valid_until,omitempty"`
+ // DockerPassword is the temporary password we use to authenticate Docker users.
+ // Generated at the time of token creation, stored here as a BCrypt hash.
+ DockerPassword string `json:"docker_password,omitempty"`
+}
+
+// NewTokenDB returns a new TokenDB structure
+func NewTokenDB(file string) (TokenDB, error) {
+ db, err := leveldb.OpenFile(file, nil)
+ return &TokenDBImpl{
+ DB: db,
+ }, err
+}
+
+func (db *TokenDBImpl) GetValue(user string) (*TokenDBValue, error) {
+ valueStr, err := db.Get(getDBKey(user), nil)
+ switch {
+ case err == leveldb.ErrNotFound:
+ return nil, nil
+ case err != nil:
+ glog.Errorf("error accessing token db: %s", err)
+ return nil, fmt.Errorf("error accessing token db: %s", err)
+ }
+ var dbv TokenDBValue
+ err = json.Unmarshal(valueStr, &dbv)
+ if err != nil {
+ glog.Errorf("bad DB value for %q (%q): %s", user, string(valueStr), err)
+ return nil, fmt.Errorf("bad DB value", err)
+ }
+ return &dbv, nil
+}
+
+func (db *TokenDBImpl) StoreToken(user string, v *TokenDBValue, updatePassword bool) (dp string, err error) {
+ if updatePassword {
+ dp = uniuri.New()
+ dph, _ := bcrypt.GenerateFromPassword([]byte(dp), bcrypt.DefaultCost)
+ v.DockerPassword = string(dph)
+ }
+
+ data, err := json.Marshal(v)
+ if err != nil {
+ return "", err
+ }
+ err = db.Put(getDBKey(user), data, nil)
+ if err != nil {
+ glog.Errorf("failed to set token data for %s: %s", user, err)
+ }
+ glog.V(2).Infof("Server tokens for %s: %s", user, string(data))
+ return
+}
+
+func (db *TokenDBImpl) ValidateToken(user string, password PasswordString) error {
+ dbv, err := db.GetValue(user)
+ if err != nil {
+ return err
+ }
+ if dbv == nil {
+ return NoMatch
+ }
+ if bcrypt.CompareHashAndPassword([]byte(dbv.DockerPassword), []byte(password)) != nil {
+ return WrongPass
+ }
+ if time.Now().After(dbv.ValidUntil) {
+ return ExpiredToken
+ }
+ return nil
+}
+
+func (db *TokenDBImpl) DeleteToken(user string) error {
+ glog.V(1).Infof("deleting token for %s", user)
+ if err := db.Delete(getDBKey(user), nil); err != nil {
+ return fmt.Errorf("failed to delete %s: %s", user, err)
+ }
+ return nil
+}
+
+func getDBKey(user string) []byte {
+ return []byte(fmt.Sprintf("%s%s", tokenDBPrefix, user))
+}
diff --git a/auth_server/server/config.go b/auth_server/server/config.go
index 4c547c15..e79aa3e1 100644
--- a/auth_server/server/config.go
+++ b/auth_server/server/config.go
@@ -23,6 +23,7 @@ import (
"fmt"
"io/ioutil"
"strings"
+ "time"
"github.com/cesanta/docker_auth/auth_server/authn"
"github.com/cesanta/docker_auth/auth_server/authz"
@@ -35,6 +36,7 @@ type Config struct {
Token TokenConfig `yaml:"token"`
Users map[string]*authn.Requirements `yaml:"users,omitempty"`
GoogleAuth *authn.GoogleAuthConfig `yaml:"google_auth,omitempty"`
+ GitHubAuth *authn.GitHubAuthConfig `yaml:"github_auth,omitempty"`
LDAPAuth *authn.LDAPAuthConfig `yaml:"ldap_auth,omitempty"`
MongoAuth *authn.MongoAuthConfig `yaml:"mongo_auth,omitempty"`
ExtAuth *authn.ExtAuthConfig `yaml:"ext_auth,omitempty"`
@@ -73,7 +75,7 @@ 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.ExtAuth == nil && c.GoogleAuth == nil && c.LDAPAuth == nil && c.MongoAuth == nil {
+ if c.Users == nil && c.ExtAuth == nil && c.GoogleAuth == nil && c.GitHubAuth == 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 {
@@ -96,6 +98,25 @@ func validate(c *Config) error {
gac.HTTPTimeout = 10
}
}
+ if ghac := c.GitHubAuth; ghac != nil {
+ if ghac.ClientSecretFile != "" {
+ contents, err := ioutil.ReadFile(ghac.ClientSecretFile)
+ if err != nil {
+ return fmt.Errorf("could not read %s: %s", ghac.ClientSecretFile, err)
+ }
+ ghac.ClientSecret = strings.TrimSpace(string(contents))
+ }
+ if ghac.ClientId == "" || ghac.ClientSecret == "" || ghac.TokenDB == "" {
+ return errors.New("github_auth.{client_id,client_secret,token_db} are required.")
+ }
+ if ghac.HTTPTimeout <= 0 {
+ ghac.HTTPTimeout = time.Duration(10 * time.Second)
+ }
+ if ghac.RevalidateAfter == 0 {
+ // Token expires after 1 hour by default
+ ghac.RevalidateAfter = time.Duration(1 * time.Hour)
+ }
+ }
if c.ExtAuth != nil {
if err := c.ExtAuth.Validate(); err != nil {
return fmt.Errorf("bad ext_auth config: %s", err)
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index a4ddea7e..2e0b6702 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -38,6 +38,7 @@ type AuthServer struct {
authenticators []authn.Authenticator
authorizers []authz.Authorizer
ga *authn.GoogleAuth
+ gha *authn.GitHubAuth
}
func NewAuthServer(c *Config) (*AuthServer, error) {
@@ -73,6 +74,14 @@ func NewAuthServer(c *Config) (*AuthServer, error) {
as.authenticators = append(as.authenticators, ga)
as.ga = ga
}
+ if c.GitHubAuth != nil {
+ gha, err := authn.NewGitHubAuth(c.GitHubAuth)
+ if err != nil {
+ return nil, err
+ }
+ as.authenticators = append(as.authenticators, gha)
+ as.gha = gha
+ }
if c.LDAPAuth != nil {
la, err := authn.NewLDAPAuth(c.LDAPAuth)
if err != nil {
@@ -180,6 +189,9 @@ func (as *AuthServer) Authenticate(ar *authRequest) (bool, error) {
if err != nil {
if err == authn.NoMatch {
continue
+ } else if err == authn.WrongPass {
+ glog.Warningf("Failed authenticateion with %s: %s", err)
+ return false, nil
}
err = fmt.Errorf("authn #%d returned error: %s", i+1, err)
glog.Errorf("%s: %s", ar, err)
@@ -297,6 +309,8 @@ func (as *AuthServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
as.doAuth(rw, req)
case req.URL.Path == "/google_auth" && as.ga != nil:
as.ga.DoGoogleAuth(rw, req)
+ case req.URL.Path == "/github_auth" && as.gha != nil:
+ as.gha.DoGitHubAuth(rw, req)
default:
http.Error(rw, "Not found", http.StatusNotFound)
return
@@ -308,7 +322,10 @@ func (as *AuthServer) doIndex(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Content-Type", "text-html; charset=utf-8")
fmt.Fprintf(rw, "%s
\n", as.config.Token.Issuer)
if as.ga != nil {
- fmt.Fprint(rw, `Login with Google account`)
+ fmt.Fprint(rw, `Login with Google account
`)
+ }
+ if as.gha != nil {
+ fmt.Fprint(rw, `Login with GitHub account
`)
}
}
diff --git a/examples/reference.yml b/examples/reference.yml
index 6216c627..fae40794 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -61,6 +61,27 @@ google_auth:
# How long to wait when talking to Google servers. Optional.
http_timeout: 10
+# GitHub authentication.
+# ==! NB: DO NOT ENTER YOUR GITHUB PASSWORD AT "docker login". IT WILL NOT WORK.
+# Instead, Auth server maintains a database of GitHub authentication tokens.
+# Go to the server's port with you browser and follow the "Login with GitHub account" link.
+# Once signed in, you will get a throw-away password which you can use for Docker login.
+google_auth:
+ # client_id and client_secret for API access. Required.
+ # You can register a new application here: https://github.com/settings/developers
+ # NB: Make sure JavaScript origins are configured correcly.
+ client_id: "1223123456"
+ # Either client_secret or client_secret_file is required. Use client_secret_file if you don't
+ # want to have sensitive information checked in.
+ # client_secret: "verysecret"
+ client_secret_file: "/path/to/client_secret.txt"
+ # Where to store server tokens. Required.
+ token_db: "/somewhere/to/put/github_tokens.ldb"
+ # How long to wait when talking to GitHub servers. Optional.
+ http_timeout: "10s"
+ # How long to wait before revalidating the GitHub token. Optional.
+ revalidate_after: "1h"
+
# LDAP authentication.
# Authentication is performed by first binding to the server, looking up the user entry
# by using the specified filter, and then re-binding using the matched DN and the password provided.
From 6208131f4cbd899a09e11bcf91a41401b9bee842 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Pinson?=
Date: Wed, 20 Jul 2016 14:05:14 +0200
Subject: [PATCH 030/209] Fix displaying of nil in logs
---
auth_server/server/server.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 2e0b6702..f8dfa56c 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -185,7 +185,7 @@ func (as *AuthServer) ParseRequest(req *http.Request) (*authRequest, error) {
func (as *AuthServer) Authenticate(ar *authRequest) (bool, error) {
for i, a := range as.authenticators {
result, err := a.Authenticate(ar.Account, ar.Password)
- glog.V(2).Infof("Authn %s %s -> %t, %s", a.Name(), ar.Account, result, err)
+ glog.V(2).Infof("Authn %s %s -> %t, %v", a.Name(), ar.Account, result, err)
if err != nil {
if err == authn.NoMatch {
continue
From 88d73f20d8abfb9c64e2b63bff7569865f46cc2e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Pinson?=
Date: Wed, 20 Jul 2016 14:00:48 +0200
Subject: [PATCH 031/209] Fix various typos
---
auth_server/server/server.go | 4 ++--
examples/reference.yml | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index f8dfa56c..1a9fe24d 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -190,7 +190,7 @@ func (as *AuthServer) Authenticate(ar *authRequest) (bool, error) {
if err == authn.NoMatch {
continue
} else if err == authn.WrongPass {
- glog.Warningf("Failed authenticateion with %s: %s", err)
+ glog.Warningf("Failed authentication with %s: %s", err)
return false, nil
}
err = fmt.Errorf("authn #%d returned error: %s", i+1, err)
@@ -357,7 +357,7 @@ func (as *AuthServer) doAuth(rw http.ResponseWriter, req *http.Request) {
return
}
} else {
- // Authenticaltion-only request ("docker login"), pass through.
+ // Authentication-only request ("docker login"), pass through.
}
token, err := as.CreateToken(ar, ares)
if err != nil {
diff --git a/examples/reference.yml b/examples/reference.yml
index fae40794..9384e0be 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -50,7 +50,7 @@ google_auth:
domain: "example.com" # Optional. If set, only logins fromt his domain are accepted.
# client_id and client_secret for API access. Required.
# Follow instructions here: https://developers.google.com/identity/sign-in/web/devconsole-project
- # NB: Make sure JavaScript origins are configured correcly.
+ # NB: Make sure JavaScript origins are configured correctly.
client_id: "1223123456-somethingsomething.apps.googleusercontent.com"
# Either client_secret or client_secret_file is required. Use client_secret_file if you don't
# want to have sensitive information checked in.
@@ -66,10 +66,10 @@ google_auth:
# Instead, Auth server maintains a database of GitHub authentication tokens.
# Go to the server's port with you browser and follow the "Login with GitHub account" link.
# Once signed in, you will get a throw-away password which you can use for Docker login.
-google_auth:
+github_auth:
# client_id and client_secret for API access. Required.
# You can register a new application here: https://github.com/settings/developers
- # NB: Make sure JavaScript origins are configured correcly.
+ # NB: Make sure JavaScript origins are configured correctly.
client_id: "1223123456"
# Either client_secret or client_secret_file is required. Use client_secret_file if you don't
# want to have sensitive information checked in.
From f4dd2b03980927fc373c73e1ea08120ca2cad365 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Pinson?=
Date: Fri, 22 Jul 2016 01:13:16 +0200
Subject: [PATCH 032/209] Add organization to reference.yml
---
examples/reference.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/examples/reference.yml b/examples/reference.yml
index 9384e0be..1c1f944a 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -67,6 +67,7 @@ google_auth:
# Go to the server's port with you browser and follow the "Login with GitHub account" link.
# Once signed in, you will get a throw-away password which you can use for Docker login.
github_auth:
+ organization: "acme" # Optional. If set, only logins from this organization are accepted.
# client_id and client_secret for API access. Required.
# You can register a new application here: https://github.com/settings/developers
# NB: Make sure JavaScript origins are configured correctly.
From 7e7fcb8bf3895f970288f7bbcb9891bfd4124a86 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Pinson?=
Date: Fri, 22 Jul 2016 01:13:33 +0200
Subject: [PATCH 033/209] Fix typo in reference.yml
---
examples/reference.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples/reference.yml b/examples/reference.yml
index 1c1f944a..a4201d86 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -47,7 +47,7 @@ users:
# Go to the server's port with you browser and follow the "Login with Google account" link.
# Once signed in, you will get a throw-away password which you can use for Docker login.
google_auth:
- domain: "example.com" # Optional. If set, only logins fromt his domain are accepted.
+ domain: "example.com" # Optional. If set, only logins from this domain are accepted.
# client_id and client_secret for API access. Required.
# Follow instructions here: https://developers.google.com/identity/sign-in/web/devconsole-project
# NB: Make sure JavaScript origins are configured correctly.
From 35e7633f257cdc2d8d08587252f6599ec13dce91 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Pinson?=
Date: Fri, 22 Jul 2016 01:39:03 +0200
Subject: [PATCH 034/209] Add 'as HTTPS' to Google/GitHub doc
---
examples/reference.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/examples/reference.yml b/examples/reference.yml
index a4201d86..3dc344ab 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -44,7 +44,7 @@ users:
# Google authentication.
# ==! NB: DO NOT ENTER YOUR GOOGLE PASSWORD AT "docker login". IT WILL NOT WORK.
# Instead, Auth server maintains a database of Google authentication tokens.
-# Go to the server's port with you browser and follow the "Login with Google account" link.
+# Go to the server's port as HTTPS with your browser and follow the "Login with Google account" link.
# Once signed in, you will get a throw-away password which you can use for Docker login.
google_auth:
domain: "example.com" # Optional. If set, only logins from this domain are accepted.
@@ -64,7 +64,7 @@ google_auth:
# GitHub authentication.
# ==! NB: DO NOT ENTER YOUR GITHUB PASSWORD AT "docker login". IT WILL NOT WORK.
# Instead, Auth server maintains a database of GitHub authentication tokens.
-# Go to the server's port with you browser and follow the "Login with GitHub account" link.
+# Go to the server's port as HTTPS with your browser and follow the "Login with GitHub account" link.
# Once signed in, you will get a throw-away password which you can use for Docker login.
github_auth:
organization: "acme" # Optional. If set, only logins from this organization are accepted.
From 45388e5eea0b7e9792cf65c6fc72791f8fd7fc06 Mon Sep 17 00:00:00 2001
From: rojer
Date: Fri, 22 Jul 2016 06:55:31 +0100
Subject: [PATCH 035/209] Mention that HTTP listener can be opened w/o TLS
Closes #122
---
examples/reference.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/examples/reference.yml b/examples/reference.yml
index 3dc344ab..8b9062d4 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -13,6 +13,7 @@ server: # Server settings.
# Address to listen on.
addr: ":5001"
# TLS certificate and key.
+ # If not specified, server will open a plain HTTP listener. In that case token.certificate and key must be provided.
certificate: "/path/to/server.pem"
key: "/path/to/server.key"
# Take client's address from the specified HTTP header instead of connection.
From 257864fd523da3a8591545cfb62f92a569a7c45f Mon Sep 17 00:00:00 2001
From: Bill Wang
Date: Fri, 22 Jul 2016 22:30:54 +1000
Subject: [PATCH 036/209] update ldap docs in README - #111
---
README.md | 2 +-
auth_server/README.md | 12 ++++++++++++
2 files changed, 13 insertions(+), 1 deletion(-)
create mode 100644 auth_server/README.md
diff --git a/README.md b/README.md
index 47786c66..4fe0513a 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ This server fills the gap and implements the protocol described [here](https://g
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
+ * LDAP bind ([demo](https://github.com/kwk/docker-registry-setup))
* MongoDB user collection
* External program
diff --git a/auth_server/README.md b/auth_server/README.md
new file mode 100644
index 00000000..34b203fb
--- /dev/null
+++ b/auth_server/README.md
@@ -0,0 +1,12 @@
+### Building local image
+
+```
+git clone https://github.com/cesanta/docker_auth.git
+cd docker_auth/auth_server
+# copy ca certificate to /etc/ssl/certs/ca-certificates.crt
+pip install gitpython
+mkdir /var/tmp/go
+export GOPATH=/var/tmp/go
+export PATH=$PATH:$GOPATH/bin
+make
+```
From 6b4e143e5a40bfa74bab238bef2a2a96039856be Mon Sep 17 00:00:00 2001
From: pandada8
Date: Thu, 4 Aug 2016 15:03:39 +0800
Subject: [PATCH 037/209] enable capture group aware variable replacement
(#124)
---
auth_server/authz/acl.go | 44 +++++++++++++++++++++++++++++++++++
auth_server/authz/acl_test.go | 3 +++
examples/reference.yml | 3 +++
3 files changed, 50 insertions(+)
diff --git a/auth_server/authz/acl.go b/auth_server/authz/acl.go
index 7865b43f..e3de1d86 100644
--- a/auth_server/authz/acl.go
+++ b/auth_server/authz/acl.go
@@ -5,7 +5,9 @@ import (
"fmt"
"net"
"path"
+ "reflect"
"regexp"
+ "strconv"
"strings"
"github.com/golang/glog"
@@ -149,6 +151,17 @@ func matchIP(ipp *string, ip net.IP) bool {
return ipnet.Contains(ip)
}
+var captureGroupRegex = regexp.MustCompile(`\$\{(.+?):(\d+)\}`)
+
+func getField(i interface{}, name string) (string, bool) {
+ s := reflect.Indirect(reflect.ValueOf(i))
+ f := reflect.Indirect(s.FieldByName(name))
+ if !f.IsValid() {
+ return "", false
+ }
+ return f.String(), true
+}
+
func (mc *MatchConditions) Matches(ai *AuthRequestInfo) bool {
vars := []string{
"${account}", regexp.QuoteMeta(ai.Account),
@@ -156,6 +169,37 @@ func (mc *MatchConditions) Matches(ai *AuthRequestInfo) bool {
"${name}", regexp.QuoteMeta(ai.Name),
"${service}", regexp.QuoteMeta(ai.Service),
}
+ for _, x := range []string{"Account", "Type", "Name"} {
+ field, _ := getField(mc, x)
+ for _, found := range captureGroupRegex.FindAllStringSubmatch(field, -1) {
+ key := strings.Title(found[1])
+ index, _ := strconv.Atoi(found[2])
+ field, has := getField(mc, key)
+ if !has {
+ glog.Errorf("No field in '%s' in MatchConditions", key)
+ continue
+ }
+ if len(field) < 2 || field[0] != '/' || field[len(field)-1] != '/' {
+ continue
+ }
+ regex, err := regexp.Compile(field[1 : len(field)-1])
+ if err != nil {
+ glog.Errorf("Invalid regex in '%s' of MatchConditions", key)
+ continue
+ }
+ info, has := getField(ai, key)
+ if !has {
+ glog.Errorf("No field in '%s' in AuthRequestInfo", key)
+ continue
+ }
+ text := regex.FindStringSubmatch(info)
+ if index < 1 || index > len(text)-1 {
+ glog.Errorf("%s: Capture group index out of range", key)
+ continue
+ }
+ vars = append(vars, found[0], text[index])
+ }
+ }
return matchString(mc.Account, ai.Account, vars) &&
matchString(mc.Type, ai.Type, vars) &&
matchString(mc.Name, ai.Name, vars) &&
diff --git a/auth_server/authz/acl_test.go b/auth_server/authz/acl_test.go
index b6052460..4b9cd753 100644
--- a/auth_server/authz/acl_test.go
+++ b/auth_server/authz/acl_test.go
@@ -64,6 +64,9 @@ func TestMatching(t *testing.T) {
{MatchConditions{Name: sp("${account}")}, AuthRequestInfo{Account: "foo", Name: "foo"}, true}, // Var subst
{MatchConditions{Name: sp("/${account}_.*/")}, AuthRequestInfo{Account: "foo", Name: "foo_x"}, true},
{MatchConditions{Name: sp("/${account}_.*/")}, AuthRequestInfo{Account: ".*", Name: "foo_x"}, false}, // Quoting
+ {MatchConditions{Account: sp(`/^(.+)@test\.com$/`), Name: sp(`${account:1}/*`)}, AuthRequestInfo{Account: "john.smith@test.com", Name: "john.smith/test"}, true},
+ {MatchConditions{Account: sp(`/^(.+)@test\.com$/`), Name: sp(`${account:3}/*`)}, AuthRequestInfo{Account: "john.smith@test.com", Name: "john.smith/test"}, false},
+ {MatchConditions{Account: sp(`/^(.+)@(.+?).test\.com$/`), Name: sp(`${account:1}-${account:2}/*`)}, AuthRequestInfo{Account: "john.smith@it.test.com", Name: "john.smith-it/test"}, true},
// IP matching
{MatchConditions{IP: sp("127.0.0.1")}, AuthRequestInfo{IP: nil}, false},
{MatchConditions{IP: sp("127.0.0.1")}, AuthRequestInfo{IP: net.IPv4(127, 0, 0, 1)}, true},
diff --git a/examples/reference.yml b/examples/reference.yml
index 8b9062d4..7dc98303 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -193,6 +193,9 @@ acl:
- match: {account: "", name: "hello-world"}
actions: ["pull"]
comment: "Anonymous users can pull \"hello-world\"."
+ - match: {account: "/^(.+)@test.com$/", name: "${account:1}/*"}
+ actions: []
+ comment: "Emit domain part of account to make it a correct repo name"
# Access is denied by default.
# (optional) Define to query ACL from a MongoDB server.
From 460199840b26fc81c30449001e38919772226cfa Mon Sep 17 00:00:00 2001
From: rojer
Date: Sun, 21 Aug 2016 16:29:49 +0100
Subject: [PATCH 038/209] Update reference.yml (#133)
---
examples/reference.yml | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/examples/reference.yml b/examples/reference.yml
index 7dc98303..25670866 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -24,8 +24,9 @@ server: # Server settings.
token: # Settings for the tokens.
issuer: "Acme auth server" # Must match issuer in the Registry config.
expiration: 900
- # It is possible configure a different certificate for tokens.
- # If not specified, server certificate is used.
+ # Token must be signed by a certificate that registry trusts, i.e. by a certificate to which a trust chain
+ # can be constructed from one of the certificates in registry's auth.token.rootcertbundle.
+ # If not specified, server's TLS certificate and key are used.
# certificate: "..."
# key: "..."
From 925daddea8db474b6aa07e68f18cee089ad6f1b7 Mon Sep 17 00:00:00 2001
From: rojer
Date: Tue, 16 Aug 2016 12:18:05 +0200
Subject: [PATCH 039/209] Fix parsing of scope
Should fix #129
---
auth_server/server/server.go | 22 ++++++++++++++++------
1 file changed, 16 insertions(+), 6 deletions(-)
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 1a9fe24d..543c392a 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -166,16 +166,26 @@ func (as *AuthServer) ParseRequest(req *http.Request) (*authRequest, error) {
if err := req.ParseForm(); err != nil {
return nil, fmt.Errorf("invalid form value")
}
+ // https://github.com/docker/distribution/blob/1b9ab303a477ded9bdd3fc97e9119fa8f9e58fca/docs/spec/auth/scope.md#resource-scope-grammar
for _, scopeStr := range req.Form["scope"] {
parts := strings.Split(scopeStr, ":")
- if len(parts) != 3 {
+ var scope authScope
+ switch len(parts) {
+ case 3:
+ scope = authScope{
+ Type: parts[0],
+ Name: parts[1],
+ Actions: strings.Split(parts[2], ","),
+ }
+ case 4:
+ scope = authScope{
+ Type: parts[0],
+ Name: parts[1] + ":" + parts[2],
+ Actions: strings.Split(parts[3], ","),
+ }
+ default:
return nil, fmt.Errorf("invalid scope: %q", scopeStr)
}
- scope := authScope{
- Type: parts[0],
- Name: parts[1],
- Actions: strings.Split(parts[2], ","),
- }
sort.Strings(scope.Actions)
ar.Scopes = append(ar.Scopes, scope)
}
From 264f9c2f7a60cd8d4d33cb608902397c9b6b7288 Mon Sep 17 00:00:00 2001
From: rojer
Date: Sat, 1 Oct 2016 19:26:17 +0100
Subject: [PATCH 040/209] Shorten default version
---
auth_server/gen_version.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/auth_server/gen_version.py b/auth_server/gen_version.py
index 79132cd7..47f5309c 100755
--- a/auth_server/gen_version.py
+++ b/auth_server/gen_version.py
@@ -36,7 +36,7 @@ def get_tag_for_commit(repo, commit):
if not dirty:
version = get_tag_for_commit(repo, repo.head.commit)
if version is None:
- version = ts.strftime('%Y%m%d%H%M%S')
+ version = ts.strftime('%Y%m%d%H')
if len(sys.argv) == 1 or sys.argv[1] == '-':
From 6ba559b635c9a55f4d7499694d33b10eb2fe3f91 Mon Sep 17 00:00:00 2001
From: Jose Rodriguez
Date: Sat, 1 Oct 2016 14:30:35 -0400
Subject: [PATCH 041/209] Always prompt consent when signing in to Google OAuth
(#138)
Do consent prompt always.
---
auth_server/authn/bindata.go | 27 +++++++++++++++++++++++--
auth_server/authn/data/google_auth.tmpl | 2 +-
2 files changed, 26 insertions(+), 3 deletions(-)
diff --git a/auth_server/authn/bindata.go b/auth_server/authn/bindata.go
index a258c62a..4e9b5464 100644
--- a/auth_server/authn/bindata.go
+++ b/auth_server/authn/bindata.go
@@ -1,5 +1,6 @@
// Code generated by go-bindata.
// sources:
+// data/github_auth.tmpl
// data/google_auth.tmpl
// DO NOT EDIT!
@@ -68,7 +69,27 @@ func (fi bindataFileInfo) Sys() interface{} {
return nil
}
-var _dataGoogle_authTmpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xdc\x56\x6d\x6b\xeb\x36\x14\xfe\x9c\xfc\x0a\xe1\x5d\xb0\xc3\x7a\x65\x76\x3f\x8c\x4b\x6e\x92\xd1\xdd\xc1\xe8\x18\x6b\x59\xbb\x4f\x63\x04\x55\x3e\x76\xd4\x2a\x92\x27\x1d\x77\x0d\x21\xff\x7d\x47\x92\xf3\xd2\x97\x84\x0e\xc6\xc6\x16\x68\x63\x9f\xa3\xf3\xf6\xe8\x79\x22\x4d\x16\xb8\xd4\x4c\x21\x2c\xbd\xb4\x2d\xc4\x27\x5c\xb5\x30\xcd\x16\x88\xed\xb8\x2c\xbd\x5c\xc0\x52\x70\xeb\x9a\xf2\xdc\xa1\x92\x1a\xb2\xd9\x70\xb2\x00\x51\xcd\x86\x8c\x4d\xbc\x74\xaa\x45\xe6\x9d\x9c\x66\x65\x29\xee\xc4\x23\x6f\xac\x6d\x34\x88\x56\x79\x2e\xed\x32\xda\x4a\xad\x6e\x7d\x79\xf7\x7b\x07\x6e\x55\x7e\xc5\x3f\xf2\x0f\xfd\x0b\x5f\x2a\xc3\xef\x7c\x36\x9b\x94\x29\xd3\x8b\xa4\xa1\x0d\x4f\x7d\xc4\x7c\x29\x75\x4c\x7b\xe7\x4b\xa9\x15\x18\x1c\xb7\x5a\x60\x6d\xdd\x92\xf2\x7c\x63\x8d\xb6\xa2\x9a\x7a\x14\x0e\x33\x26\xfc\xca\x48\x56\x41\x0d\xee\xb5\x02\xb3\xe1\x60\x50\x77\x46\xa2\xb2\x86\xd1\x98\xf2\xfe\x47\xdb\x28\x53\x8c\xd8\x9a\x3c\x83\x07\xe1\x98\xe8\x70\xf1\x81\x4d\x59\x43\xe5\x79\x7c\xe1\x0d\xe0\x39\x3d\x5c\x18\x2a\x62\x24\x14\xa3\x4f\x61\xb1\xaa\x59\x91\xfc\xca\x5f\xab\xc6\x40\x75\x61\xc2\xd2\x62\xd4\x67\x1b\xbc\x2b\xf2\x2f\x1c\xf8\x4e\x63\x3e\xe2\x08\x8f\x58\xe4\x0f\x42\xab\x4a\xa0\x32\x0d\x83\x47\xe5\xe3\x03\xda\x7b\x30\x9c\xf3\x3c\xe5\x8d\x5d\xa8\x6a\x1e\xcd\xd4\x48\xaa\x21\x3b\xe7\x68\xf4\x5f\x3c\xb8\x54\x64\xdb\xd5\xcf\xe0\x5b\x6b\x3c\x75\xc5\xb7\x41\x29\xcd\x3b\x1e\xf6\xa1\x48\xad\x0c\xc2\x0e\x8f\x59\x7e\x75\x79\x7d\x93\x9f\x25\x53\xe7\x34\x59\xca\x84\xf0\x3c\x94\xd9\x7a\xa4\x35\x48\xc5\x6e\x52\x8c\x68\x5b\xad\xa4\x08\x98\xd1\x1e\x58\xf3\x89\x90\x13\xce\x03\x4e\x3b\xac\xdf\x7f\xdc\x06\xb5\xce\x4a\xf0\xfe\x3b\x81\x62\xcc\x6a\xa1\x3d\xf4\x8e\x2a\x5a\x7e\xb8\xbe\xfc\x89\x7b\x74\x34\xb1\xaa\x57\xc5\x3a\x17\x71\x17\x72\x2a\x10\x37\x22\x3f\x63\x79\xec\x9e\x2c\xdb\x41\x36\xa3\x3e\x85\xef\x64\xc8\x4d\x79\xfb\xcd\x2b\x12\xae\x5b\xa4\x5f\xc3\xba\x5f\x91\xc0\x18\x6c\xfa\x4c\xe0\x9c\x75\x07\x79\x1e\x17\xee\x54\x92\xbc\x5f\x9f\xb3\x2f\x19\x2d\xe5\xae\x47\xfb\x86\x9c\xcf\x52\x6f\xd2\xfb\x86\x01\x8d\xde\xa7\x24\x20\xbd\x25\xfa\x6a\xdb\x14\xb9\xb1\xc8\xe8\xa1\x81\x8a\x29\xd3\x6f\xf6\x66\x18\xfe\x18\x7d\x76\xb4\x8c\x4c\x8e\x8c\x64\xf1\x13\x89\x18\x38\x5e\xe4\x91\x0a\x04\xd4\xae\xfb\x6d\xeb\x07\x64\x55\x46\x61\xb1\x4e\x42\x99\xab\x8a\x5a\x5f\xaf\xf9\xe7\xf8\x7a\x51\x6d\x36\xf9\x86\x26\x5b\x80\x29\xf6\xec\xef\x3b\xa1\xaf\x50\x2e\x74\xb3\x57\xce\xa4\x4c\xba\x9f\xdc\xda\x6a\x45\xea\x99\xdc\x76\x88\xd4\xa4\xaa\xa6\x99\x27\xd2\x2b\xf3\x6d\x34\x64\xb3\x20\x01\x9a\x8b\xfd\xa1\x70\xc1\xbe\x8f\x9c\x9a\x94\x69\x75\x88\xdb\xeb\x2f\x80\x7c\x18\x4a\x50\x53\xb7\xf2\xbe\x78\x3e\x55\x59\xb2\xb0\xee\xc2\x7c\x16\x5a\xdf\x0a\x79\x1f\x74\xad\x4c\x84\x8f\x50\x82\x96\x7d\xcd\xff\xb2\x6a\x7b\x9f\x13\x06\x2f\xeb\x5a\x53\xba\xf3\x48\x2d\x22\xa4\x83\x4a\x39\x90\x38\xef\x9c\x0a\xb4\x6c\xad\xc7\x25\xb9\x44\x03\x3b\xd4\x76\x3d\x8a\x24\xbd\x03\x0e\x1e\xee\xf5\x81\xf7\xbf\x26\xc6\x00\xf9\x9c\xf8\x49\x72\x94\xb6\x02\x32\xed\x87\xf9\x35\x99\x7e\xfb\x1b\x75\xf9\x04\xb6\x2c\xf9\xc6\xd9\x19\xfb\xc7\xd4\xfb\x54\xa3\x29\x80\x86\x7f\xa3\xd8\xe3\x57\xfc\xbf\xd7\xcc\x0b\x91\x5c\x76\xf8\x44\x25\xb6\xc3\xd3\xd2\xd8\x05\x1c\xd5\xc6\xff\xea\xa4\x22\xa1\x5f\x81\x0b\x07\x3a\xa3\xa0\x07\x70\xef\xbd\xaa\x20\x8a\x3f\x60\xc5\xff\x15\x05\xbd\x4d\x28\xd4\xde\xc9\x83\xeb\xa8\x0e\x5f\x2a\x87\xf6\xe2\x18\xcd\x0f\x5c\x74\x11\x6a\x35\x20\x1c\x13\xc1\x13\x36\x6f\x11\xec\xd5\x74\x8a\xd7\x83\x53\x2c\xe2\x95\xa2\xbb\xa2\x31\xf4\xe3\x58\xec\x02\x5e\xe1\x8c\x8f\xe4\x0a\x15\xf3\x37\x1c\x8e\xaf\x24\x38\x7a\x44\x3e\x57\x58\xa5\x1e\xa2\xbc\x52\x7c\xb8\x4c\x92\x25\x1c\x57\xe9\x9c\xa2\x63\x8b\x2e\xb9\xb3\xe1\x9f\x01\x00\x00\xff\xff\xf6\x19\xb6\xcf\xec\x0a\x00\x00")
+var _dataGithub_authTmpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xac\x90\xb1\x6e\xf4\x20\x10\x84\x7b\x3f\x05\xa2\xf8\xaf\x33\xfd\x09\xfc\x2b\x4a\x91\x44\x4a\x95\x17\x88\x30\xde\x98\xd5\x61\x16\xc1\x92\xe8\x72\xba\x77\x0f\xc6\x45\x5e\x20\x0d\xda\x91\x98\x6f\x46\xa3\x3d\x6f\x41\x20\xc3\x56\x1c\x25\xe8\x17\x5f\x13\x18\xe9\x99\xd3\x59\xa9\xe2\x3c\x6c\x76\xa4\xbc\xaa\x87\xcc\xe8\x02\xc8\x69\xd0\x33\x2d\xd7\x69\x10\x42\xcf\x95\x99\xa2\x38\x2c\x87\x90\x82\xa2\x0b\xe8\x2e\x46\x06\x72\x96\x91\xe2\xe8\x33\x7c\x98\xd3\x8e\x2c\x8d\xb9\x22\xfb\x3a\x8f\x8e\x36\x15\x68\xc5\xa8\xc8\x56\xf6\x6a\x7f\x28\xe3\x37\xfc\xef\x5d\x4c\x2d\x90\xcf\x2d\x1c\xc3\xbf\xc6\x83\xc8\xef\xb8\x98\xdb\x6d\x7c\xec\xe2\x65\xb9\xdf\x4f\x72\x7a\xdd\x01\xe2\xab\x11\xc5\x13\xf2\x73\x9d\xb5\x3a\x6a\xfc\x49\xbd\x02\xcc\x18\xd7\xa2\x6c\x4a\xcd\xd3\x7f\x97\x96\xfa\x06\x9f\x74\x01\x61\x9d\x83\x52\x7e\x13\xdb\xd5\x87\xd1\x6a\x9f\x75\x1a\x7e\x02\x00\x00\xff\xff\x70\x1a\x30\x49\x5e\x01\x00\x00")
+
+func dataGithub_authTmplBytes() ([]byte, error) {
+ return bindataRead(
+ _dataGithub_authTmpl,
+ "data/github_auth.tmpl",
+ )
+}
+
+func dataGithub_authTmpl() (*asset, error) {
+ bytes, err := dataGithub_authTmplBytes()
+ if err != nil {
+ return nil, err
+ }
+
+ info := bindataFileInfo{name: "data/github_auth.tmpl", size: 350, mode: os.FileMode(420), modTime: time.Unix(1, 0)}
+ a := &asset{bytes: bytes, info: info}
+ return a, nil
+}
+
+var _dataGoogle_authTmpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xdc\x56\x6d\x6b\xeb\x36\x14\xfe\x9c\xfc\x0a\xe1\x5d\xb0\xc3\x7a\x65\x76\x3f\x8c\x4b\x6e\x92\xd1\xdd\xc1\xe8\x18\x6b\x59\xbb\x4f\x63\x04\x55\x3e\x76\xd4\x2a\x92\x27\x1d\x77\x0d\x21\xff\x7d\x47\x92\xf3\xd2\x97\x84\x0e\xc6\xc6\x16\x68\x6d\x9f\xa3\xf3\x9c\x17\x3d\x8f\xe5\xc9\x02\x97\x9a\x29\x84\xa5\x97\xb6\x85\x78\x87\xab\x16\xa6\xd9\x02\xb1\x1d\x97\xa5\x97\x0b\x58\x0a\x6e\x5d\x53\x9e\x3b\x54\x52\x43\x36\x1b\x4e\x16\x20\xaa\xd9\x90\xb1\x89\x97\x4e\xb5\xc8\xbc\x93\xd3\xac\x2c\xc5\x9d\x78\xe4\x8d\xb5\x8d\x06\xd1\x2a\xcf\xa5\x5d\x46\x5b\xa9\xd5\xad\x2f\xef\x7e\xef\xc0\xad\xca\xaf\xf8\x47\xfe\xa1\x7f\xe0\x4b\x65\xf8\x9d\xcf\x66\x93\x32\x21\xbd\x00\x0d\x65\x78\xaa\x23\xe2\x25\xe8\x08\x7b\xe7\x4b\xa9\x15\x18\x1c\xb7\x5a\x60\x6d\xdd\x92\x70\xbe\xb1\x46\x5b\x51\x4d\x3d\x0a\x87\x19\x13\x7e\x65\x24\xab\xa0\x06\xf7\x5a\x82\xd9\x70\x30\xa8\x3b\x23\x51\x59\xc3\xa8\x4d\x79\xff\xa3\x6d\x94\x29\x46\x6c\x4d\x9e\xc1\x83\x70\x4c\x74\xb8\xf8\xc0\xa6\xac\xa1\xf4\x3c\x3e\xf0\x06\xf0\x9c\x6e\x2e\x0c\x25\x31\x12\x8a\xd1\xa7\xb0\x58\xd5\xac\x48\x7e\xe5\xaf\x55\x63\xa0\xba\x30\x61\x69\x31\xea\xd1\x06\xef\x8a\xfc\x0b\x07\xbe\xd3\x98\x8f\x38\xc2\x23\x16\xf9\x83\xd0\xaa\x12\xa8\x4c\xc3\xe0\x51\xf9\x78\x83\xf6\x1e\x0c\xe7\x3c\x4f\xb8\xb1\x0a\x55\xcd\xa3\x99\x0a\x49\x39\x64\xe7\x1c\xb5\xfe\x8b\x07\x97\x92\x6c\xab\xfa\x19\x7c\x6b\x8d\xa7\xaa\xf8\x36\x28\xc1\xbc\xe3\x61\x1f\x8a\x54\xca\x20\xec\xf0\x98\xe5\x57\x97\xd7\x37\xf9\x59\x32\x75\x4e\x93\xa5\x4c\x13\x9e\x87\x34\x5b\x8f\xb4\x06\x29\xd9\x4d\x8a\x11\x6d\xab\x95\x14\x61\x66\xb4\x07\xd6\x7c\xa2\xc9\x09\xe7\x01\xa7\x1d\xd6\xef\x3f\x6e\x83\x5a\x67\x25\x78\xff\x9d\x40\x31\x66\xb5\xd0\x1e\x7a\x47\x15\x2d\x3f\x5c\x5f\xfe\xc4\x3d\x3a\xea\x58\xd5\xab\x62\x9d\x8b\xb8\x0b\x39\x25\x88\x1b\x91\x9f\xb1\x3c\x56\x4f\x96\x6d\x23\x9b\x51\x0f\xe1\x3b\x19\xb0\x09\xb7\xdf\xbc\x22\xcd\x75\x3b\xe9\xd7\x66\xdd\xaf\x48\xc3\x18\x6c\x7a\x24\x70\xce\xba\x03\x9c\xc7\x85\x3b\x05\x92\xf7\xeb\x73\xf6\x25\xa3\xa5\xdc\xf5\xd3\xbe\x21\xe7\x33\xe8\x4d\x7a\xde\x30\xa0\xd6\x7b\x48\x1a\xa4\xb7\x44\x5f\x6d\x9b\x22\x37\x16\x19\xdd\x34\x50\x31\x65\xfa\xcd\xde\x0c\xc3\x1f\xa3\xdf\x8e\x96\x91\xc9\x91\x91\x2c\xfe\x22\x11\x03\xc7\x8b\x3c\x52\x81\x06\xb5\xab\x7e\x5b\xfa\x01\x59\x95\x51\x58\xac\x93\x50\xe6\xaa\xa2\xd2\xd7\x6b\xfe\x39\x3e\x5e\x54\x9b\x4d\xbe\xa1\xce\x16\x60\x8a\x3d\xfb\xfb\x4a\xe8\x12\xd2\x85\x6a\xf6\xca\x99\x94\x49\xf7\x93\x5b\x5b\xad\x48\x3d\x93\xdb\x0e\x91\x8a\x54\xd5\x34\xf3\x44\x7a\x65\xbe\x8d\x86\x6c\x16\x24\x40\x7d\xb1\x3f\x14\x2e\xd8\xf7\x91\x53\x93\x32\xad\x0e\x71\x7b\xfd\x85\x21\x1f\x86\xd2\xa8\xa9\x5a\x79\x5f\x3c\xef\xaa\x2c\x59\x58\x77\x61\x3e\x0b\xad\x6f\x85\xbc\x0f\xba\x56\x26\x8e\x8f\xa6\x04\x2d\xfb\x9a\xff\x65\xd5\xf6\x3e\x27\x0c\x5e\xd6\xb5\x26\xb8\xf3\x48\x2d\x22\xa4\x83\x4a\x39\x90\x38\xef\x9c\x0a\xb4\x6c\xad\xc7\x25\xb9\x44\x03\x81\x9c\xc4\xef\x65\x8b\x91\xaf\x81\x01\x06\x77\xa3\xdc\x15\x2e\x92\x1e\x0f\x88\x79\x48\x80\x03\xef\x7f\x4d\xa1\x61\x1f\xe6\x44\xda\xb3\xd0\x7c\x05\x64\xda\x37\xf3\x6b\x32\xfd\xf6\x37\x8a\xf5\xc9\xd8\xb2\xe4\x1b\x67\x67\xec\x1f\x93\xf4\x53\xe1\xa6\x00\x6a\xfe\x8d\x6f\x80\x78\x89\xff\xf7\x42\x7a\xa1\x9c\xcb\x0e\x9f\x48\xc7\x76\x78\x5a\x2f\xbb\x80\xa3\x82\xf9\x5f\x1d\x5f\xa4\xfe\x2b\x70\xe1\x94\x67\x14\xf4\x00\xee\xbd\x57\x15\xc4\x37\x42\x98\x15\xff\x57\x14\xf4\x36\xa1\x50\x79\x27\x4f\xb3\xa3\x3a\x7c\xa9\x1c\xda\x8b\x63\x34\x3f\x70\xd1\xd7\x51\xab\x01\xe1\x98\x08\x9e\xb0\x79\x3b\xc1\x5e\x4d\xa7\x78\x3d\x38\xc5\x22\x5e\x29\xfa\x80\x34\x86\xde\x98\xc5\x2e\xe0\x15\xce\xf8\x48\xae\x90\x31\x7f\xc3\x89\xf9\x0a\xc0\xd1\x73\xf3\xb9\xc2\x2a\xf5\x10\xe5\x95\xe2\xc3\x17\x26\x59\xc2\x19\x96\x0e\x2f\x3a\xcb\xe8\xcb\x77\x36\xfc\x33\x00\x00\xff\xff\x8d\x6b\x3d\x03\x01\x0b\x00\x00")
func dataGoogle_authTmplBytes() ([]byte, error) {
return bindataRead(
@@ -83,7 +104,7 @@ func dataGoogle_authTmpl() (*asset, error) {
return nil, err
}
- info := bindataFileInfo{name: "data/google_auth.tmpl", size: 2796, mode: os.FileMode(420), modTime: time.Unix(1, 0)}
+ info := bindataFileInfo{name: "data/google_auth.tmpl", size: 2817, mode: os.FileMode(420), modTime: time.Unix(1, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@@ -140,6 +161,7 @@ func AssetNames() []string {
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
+ "data/github_auth.tmpl": dataGithub_authTmpl,
"data/google_auth.tmpl": dataGoogle_authTmpl,
}
@@ -184,6 +206,7 @@ type bintree struct {
}
var _bintree = &bintree{nil, map[string]*bintree{
"data": &bintree{nil, map[string]*bintree{
+ "github_auth.tmpl": &bintree{dataGithub_authTmpl, map[string]*bintree{}},
"google_auth.tmpl": &bintree{dataGoogle_authTmpl, map[string]*bintree{}},
}},
}}
diff --git a/auth_server/authn/data/google_auth.tmpl b/auth_server/authn/data/google_auth.tmpl
index b4aee197..0607e5b5 100644
--- a/auth_server/authn/data/google_auth.tmpl
+++ b/auth_server/authn/data/google_auth.tmpl
@@ -38,7 +38,7 @@
$('#signinButton').click(function() {
// signInCallback defined in step 6.
var auth2 = gapi.auth2.getAuthInstance();
- auth2.grantOfflineAccess({'redirect_uri': 'postmessage'}).then(function(authResult) {
+ auth2.grantOfflineAccess({'redirect_uri': 'postmessage', 'prompt': 'consent'}).then(function(authResult) {
console.log(authResult);
$.ajax({
type: 'POST',
From 945fff38fe166fe16d21696ce51d1f9f74521bb3 Mon Sep 17 00:00:00 2001
From: rojer
Date: Sat, 1 Oct 2016 20:17:39 +0100
Subject: [PATCH 042/209] Fix building
For some reason adding file to root dir fails, put it under /docker_auth
---
auth_server/Dockerfile | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/auth_server/Dockerfile b/auth_server/Dockerfile
index 8c237586..898026a3 100644
--- a/auth_server/Dockerfile
+++ b/auth_server/Dockerfile
@@ -1,6 +1,6 @@
FROM busybox
-EXPOSE 5001
-ENTRYPOINT ["/auth_server"]
-CMD ["/config/auth_config.yml"]
+ADD auth_server /docker_auth/
COPY ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
-COPY auth_server .
+ENTRYPOINT ["/docker_auth/auth_server"]
+CMD ["/config/auth_config.yml"]
+EXPOSE 5001
From 24904e177a6515b2f312eb2ef715eb76f1a2978e Mon Sep 17 00:00:00 2001
From: rojer
Date: Sat, 1 Oct 2016 20:19:37 +0100
Subject: [PATCH 043/209] Run generate on docker-build
Keeps bindata in sync, updates version
---
auth_server/Makefile | 1 +
1 file changed, 1 insertion(+)
diff --git a/auth_server/Makefile b/auth_server/Makefile
index 879a7c12..f716b448 100644
--- a/auth_server/Makefile
+++ b/auth_server/Makefile
@@ -28,6 +28,7 @@ ca-certificates.crt:
cp $(CA_BUNDLE) .
docker-build:
+ go generate ./...
docker run --rm -v $(PWD):/src -e COMPRESS_BINARY=$(COMPRESS_BINARY) $(BUILDER_OPTS-$@) $(BUILDER_IMAGE)$(BUILDER_IMAGE_EXTRA-$@) $(IMAGE)
@echo === Built version $(VERSION) ===
From c5ab8eac33207794386b97cf138f0c6ee54389c8 Mon Sep 17 00:00:00 2001
From: Jose Rodriguez
Date: Sat, 1 Oct 2016 15:24:52 -0400
Subject: [PATCH 044/209] Add positional argument for real ip header. (#137)
---
auth_server/server/config.go | 1 +
auth_server/server/server.go | 12 +++++++++++-
examples/reference.yml | 3 +++
3 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/auth_server/server/config.go b/auth_server/server/config.go
index e79aa3e1..68576c2c 100644
--- a/auth_server/server/config.go
+++ b/auth_server/server/config.go
@@ -47,6 +47,7 @@ type Config struct {
type ServerConfig struct {
ListenAddress string `yaml:"addr,omitempty"`
RealIPHeader string `yaml:"real_ip_header,omitempty"`
+ RealIPPos int `yaml:"real_ip_pos,omitempty"`
CertFile string `yaml:"certificate,omitempty"`
KeyFile string `yaml:"key,omitempty"`
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 543c392a..6ffa00df 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -141,7 +141,17 @@ func (as *AuthServer) ParseRequest(req *http.Request) (*authRequest, error) {
ar := &authRequest{RemoteConnAddr: req.RemoteAddr, RemoteAddr: req.RemoteAddr}
if as.config.Server.RealIPHeader != "" {
hv := req.Header.Get(as.config.Server.RealIPHeader)
- ar.RemoteAddr = strings.TrimSpace(strings.Split(hv, ",")[0])
+ ips := strings.Split(hv, ",")
+
+ realIPPos := as.config.Server.RealIPPos
+ if realIPPos < 0 {
+ realIPPos = len(ips) + realIPPos
+ if realIPPos < 0 {
+ realIPPos = 0
+ }
+ }
+
+ ar.RemoteAddr = strings.TrimSpace(ips[realIPPos])
glog.V(3).Infof("Conn ip %s, %s: %s, addr: %s", ar.RemoteAddr, as.config.Server.RealIPHeader, hv, ar.RemoteAddr)
if ar.RemoteAddr == "" {
return nil, fmt.Errorf("client address not provided")
diff --git a/examples/reference.yml b/examples/reference.yml
index 25670866..220e6298 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -20,6 +20,9 @@ server: # Server settings.
# May be useful if the server is behind a proxy or load balancer.
# If configured, this header must be present, requests without it will be rejected.
# real_ip_header: "X-Forwarded-For"
+ # Optional position of client ip in X-Forwarded-For, negative starts from
+ # end of addresses.
+ # real_ip_pos: -2
token: # Settings for the tokens.
issuer: "Acme auth server" # Must match issuer in the Registry config.
From f62ab765112609f5a6d4cf16d008d11ad78f1300 Mon Sep 17 00:00:00 2001
From: rojer
Date: Sat, 8 Oct 2016 00:59:30 +0100
Subject: [PATCH 045/209] Auth labels
Add labels that can be passed from authentication to authorization rules.
Add ability to match them:
acl:
- match: {labels: {"foo": "bar", "baz": "/quux.*/"}}
All the label expressions must match ("and" logic on label expressions).
Labels are a string -> []string map, so a label can have multiple values
and all of them will be tried during matching, but only one is required
("or" logic on label values).
For now, only external authenticator can add labels, but in the future
e.g. GitHub authn can pass down GH organizations or teams the user
belongs to (should help #117).
---
auth_server/authn/authn.go | 4 ++-
auth_server/authn/ext_auth.go | 36 +++++++++--------------
auth_server/authn/github_auth.go | 8 +++---
auth_server/authn/google_auth.go | 8 +++---
auth_server/authn/ldap_auth.go | 20 ++++++-------
auth_server/authn/mongo_auth.go | 6 ++--
auth_server/authn/static_auth.go | 8 +++---
auth_server/authz/acl.go | 49 ++++++++++++++++++++++++++------
auth_server/authz/acl_test.go | 12 ++++++++
auth_server/authz/authz.go | 3 ++
auth_server/main.go | 4 +--
auth_server/server/config.go | 4 +++
auth_server/server/server.go | 21 ++++++++------
13 files changed, 116 insertions(+), 67 deletions(-)
diff --git a/auth_server/authn/authn.go b/auth_server/authn/authn.go
index b7af6531..bdb17afa 100644
--- a/auth_server/authn/authn.go
+++ b/auth_server/authn/authn.go
@@ -18,6 +18,8 @@ package authn
import "errors"
+type Labels map[string][]string
+
// Authentication plugin interface.
type Authenticator interface {
// Given a user name and a password (plain text), responds with the result or an error.
@@ -26,7 +28,7 @@ type Authenticator interface {
// e.g. none of the rules matched.
// Another special WrongPass error is returned if the authorizer failed to authenticate.
// Implementations must be goroutine-safe.
- Authenticate(user string, password PasswordString) (bool, error)
+ Authenticate(user string, password PasswordString) (bool, Labels, error)
// Finalize resources in preparation for shutdown.
// When this call is made there are guaranteed to be no Authenticate requests in flight
diff --git a/auth_server/authn/ext_auth.go b/auth_server/authn/ext_auth.go
index d7a85a73..9a1709ef 100644
--- a/auth_server/authn/ext_auth.go
+++ b/auth_server/authn/ext_auth.go
@@ -31,11 +31,6 @@ type ExtAuthConfig struct {
Args []string `yaml:"args"`
}
-type ExtAuthRequest struct {
- User string `json:"user"`
- Password string `json:"password"`
-}
-
type ExtAuthStatus int
const (
@@ -46,8 +41,7 @@ const (
)
type ExtAuthResponse struct {
- Status int `json:"status"`
- Message string `json:"message,omitempty"`
+ Labels Labels `json:"labels,omitempty"`
}
func (c *ExtAuthConfig) Validate() error {
@@ -64,23 +58,15 @@ type extAuth struct {
cfg *ExtAuthConfig
}
-func (r ExtAuthRequest) String() string {
- if r.Password != "" {
- r.Password = "***"
- }
- b, _ := json.Marshal(r)
- return string(b)
-}
-
func NewExtAuth(cfg *ExtAuthConfig) *extAuth {
glog.Infof("External authenticator: %s %s", cfg.Command, strings.Join(cfg.Args, " "))
return &extAuth{cfg: cfg}
}
-func (ea *extAuth) Authenticate(user string, password PasswordString) (bool, error) {
+func (ea *extAuth) Authenticate(user string, password PasswordString) (bool, Labels, error) {
cmd := exec.Command(ea.cfg.Command, ea.cfg.Args...)
cmd.Stdin = strings.NewReader(fmt.Sprintf("%s %s", user, string(password)))
- _, err := cmd.Output()
+ output, err := cmd.Output()
es := 0
et := ""
if err == nil {
@@ -91,18 +77,24 @@ func (ea *extAuth) Authenticate(user string, password PasswordString) (bool, err
es = int(ExtAuthError)
et = fmt.Sprintf("cmd run error: %s", err)
}
- glog.V(2).Infof("%s %s -> %d", cmd.Path, cmd.Args, es)
+ glog.V(2).Infof("%s %s -> %d %s", cmd.Path, cmd.Args, es, output)
switch ExtAuthStatus(es) {
case ExtAuthAllowed:
- return true, nil
+ var resp ExtAuthResponse
+ if len(output) > 0 {
+ if err = json.Unmarshal(output, &resp); err != nil {
+ return false, nil, err
+ }
+ }
+ return true, resp.Labels, nil
case ExtAuthDenied:
- return false, nil
+ return false, nil, nil
case ExtAuthNoMatch:
- return false, NoMatch
+ return false, nil, NoMatch
default:
glog.Errorf("Ext command error: %d %s", es, et)
}
- return false, fmt.Errorf("bad return code from command: %d", es)
+ return false, nil, fmt.Errorf("bad return code from command: %d", es)
}
func (sua *extAuth) Stop() {
diff --git a/auth_server/authn/github_auth.go b/auth_server/authn/github_auth.go
index 67540561..9bef12b2 100644
--- a/auth_server/authn/github_auth.go
+++ b/auth_server/authn/github_auth.go
@@ -235,17 +235,17 @@ func (gha *GitHubAuth) validateServerToken(user string) (*TokenDBValue, error) {
return v, nil
}
-func (gha *GitHubAuth) Authenticate(user string, password PasswordString) (bool, error) {
+func (gha *GitHubAuth) Authenticate(user string, password PasswordString) (bool, Labels, error) {
err := gha.db.ValidateToken(user, password)
if err == ExpiredToken {
_, err = gha.validateServerToken(user)
if err != nil {
- return false, err
+ return false, nil, err
}
} else if err != nil {
- return false, err
+ return false, nil, err
}
- return true, nil
+ return true, nil, nil
}
func (gha *GitHubAuth) Stop() {
diff --git a/auth_server/authn/google_auth.go b/auth_server/authn/google_auth.go
index c0da268e..cd0704cb 100644
--- a/auth_server/authn/google_auth.go
+++ b/auth_server/authn/google_auth.go
@@ -399,17 +399,17 @@ func (ga *GoogleAuth) doGoogleAuthSignOut(rw http.ResponseWriter, token string)
fmt.Fprint(rw, "signed out")
}
-func (ga *GoogleAuth) Authenticate(user string, password PasswordString) (bool, error) {
+func (ga *GoogleAuth) Authenticate(user string, password PasswordString) (bool, Labels, error) {
err := ga.db.ValidateToken(user, password)
if err == ExpiredToken {
_, err = ga.validateServerToken(user)
if err != nil {
- return false, err
+ return false, nil, err
}
} else if err != nil {
- return false, err
+ return false, nil, err
}
- return true, nil
+ return true, nil, nil
}
func (ga *GoogleAuth) Stop() {
diff --git a/auth_server/authn/ldap_auth.go b/auth_server/authn/ldap_auth.go
index 769a78dc..a885b40c 100644
--- a/auth_server/authn/ldap_auth.go
+++ b/auth_server/authn/ldap_auth.go
@@ -53,19 +53,19 @@ func NewLDAPAuth(c *LDAPAuthConfig) (*LDAPAuth, error) {
}
//How to authenticate user, please refer to https://github.com/go-ldap/ldap/blob/master/example_test.go#L166
-func (la *LDAPAuth) Authenticate(account string, password PasswordString) (bool, error) {
+func (la *LDAPAuth) Authenticate(account string, password PasswordString) (bool, Labels, error) {
if account == "" {
- return false, NoMatch
+ return false, nil, NoMatch
}
l, err := la.ldapConnection()
if err != nil {
- return false, err
+ return false, nil, err
}
defer l.Close()
// First bind with a read only user, to prevent the following search won't perform any write action
if bindErr := la.bindReadOnlyUser(l); bindErr != nil {
- return false, bindErr
+ return false, nil, bindErr
}
account = la.escapeAccountInput(account)
@@ -73,27 +73,27 @@ func (la *LDAPAuth) Authenticate(account string, password PasswordString) (bool,
filter := la.getFilter(account)
accountEntryDN, uSearchErr := la.ldapSearch(l, &la.config.Base, &filter, &[]string{})
if uSearchErr != nil {
- return false, uSearchErr
+ return false, nil, uSearchErr
}
if accountEntryDN == "" {
- return false, NoMatch // User does not exist
+ return false, nil, NoMatch // User does not exist
}
// Bind as the user to verify their password
if len(accountEntryDN) > 0 {
err := l.Bind(accountEntryDN, string(password))
if err != nil {
if ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials) {
- return false, nil
+ return false, nil, nil
}
- return false, err
+ return false, nil, err
}
}
// Rebind as the read only user for any futher queries
if bindErr := la.bindReadOnlyUser(l); bindErr != nil {
- return false, bindErr
+ return false, nil, bindErr
}
- return true, nil
+ return true, nil, nil
}
func (la *LDAPAuth) bindReadOnlyUser(l *ldap.Conn) error {
diff --git a/auth_server/authn/mongo_auth.go b/auth_server/authn/mongo_auth.go
index 4fd36d25..165fee40 100644
--- a/auth_server/authn/mongo_auth.go
+++ b/auth_server/authn/mongo_auth.go
@@ -82,7 +82,7 @@ func NewMongoAuth(c *MongoAuthConfig) (*MongoAuth, error) {
}, nil
}
-func (mauth *MongoAuth) Authenticate(account string, password PasswordString) (bool, error) {
+func (mauth *MongoAuth) Authenticate(account string, password PasswordString) (bool, Labels, error) {
for true {
result, err := mauth.authenticate(account, password)
if err == io.EOF {
@@ -90,10 +90,10 @@ func (mauth *MongoAuth) Authenticate(account string, password PasswordString) (b
time.Sleep(time.Second)
continue
}
- return result, err
+ return result, nil, err
}
- return false, errors.New("Unable to communicate with Mongo.")
+ return false, nil, errors.New("Unable to communicate with Mongo.")
}
func (mauth *MongoAuth) authenticate(account string, password PasswordString) (bool, error) {
diff --git a/auth_server/authn/static_auth.go b/auth_server/authn/static_auth.go
index 248f7304..e7868e0e 100644
--- a/auth_server/authn/static_auth.go
+++ b/auth_server/authn/static_auth.go
@@ -44,17 +44,17 @@ func NewStaticUserAuth(users map[string]*Requirements) *staticUsersAuth {
return &staticUsersAuth{users: users}
}
-func (sua *staticUsersAuth) Authenticate(user string, password PasswordString) (bool, error) {
+func (sua *staticUsersAuth) Authenticate(user string, password PasswordString) (bool, Labels, error) {
reqs := sua.users[user]
if reqs == nil {
- return false, NoMatch
+ return false, nil, NoMatch
}
if reqs.Password != nil {
if bcrypt.CompareHashAndPassword([]byte(*reqs.Password), []byte(password)) != nil {
- return false, nil
+ return false, nil, nil
}
}
- return true, nil
+ return true, nil, nil
}
func (sua *staticUsersAuth) Stop() {
diff --git a/auth_server/authz/acl.go b/auth_server/authz/acl.go
index e3de1d86..e58e7fd2 100644
--- a/auth_server/authz/acl.go
+++ b/auth_server/authz/acl.go
@@ -10,6 +10,7 @@ import (
"strconv"
"strings"
+ "github.com/cesanta/docker_auth/auth_server/authn"
"github.com/golang/glog"
)
@@ -22,10 +23,11 @@ type ACLEntry struct {
}
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"`
- IP *string `yaml:"ip,omitempty" json:"ip,omitempty"`
+ Account *string `yaml:"account,omitempty" json:"account,omitempty"`
+ Type *string `yaml:"type,omitempty" json:"type,omitempty"`
+ Name *string `yaml:"name,omitempty" json:"name,omitempty"`
+ IP *string `yaml:"ip,omitempty" json:"ip,omitempty"`
+ Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
}
type aclAuthorizer struct {
@@ -77,17 +79,30 @@ func validateMatchConditions(mc *MatchConditions) error {
return fmt.Errorf("invalid IP pattern: %s", err)
}
}
+ for k, v := range mc.Labels {
+ err := validatePattern(v)
+ if err != nil {
+ return fmt.Errorf("invalid match pattern %q for label %s: %s", v, k, err)
+ }
+ }
return nil
}
-// NewACLAuthorizer Creates a new static authorizer with ACL that have been read from the config file
-func NewACLAuthorizer(acl ACL) (Authorizer, error) {
+func ValidateACL(acl ACL) error {
for i, e := range acl {
err := validateMatchConditions(e.Match)
if err != nil {
- return nil, fmt.Errorf("entry %d, invalid match conditions: %s", i, err)
+ return fmt.Errorf("entry %d, invalid match conditions: %s", i, err)
}
}
+ return nil
+}
+
+// NewACLAuthorizer Creates a new static authorizer with ACL that have been read from the config file
+func NewACLAuthorizer(acl ACL) (Authorizer, error) {
+ if err := ValidateACL(acl); err != nil {
+ return nil, err
+ }
glog.V(1).Infof("Created ACL Authorizer with %d entries", len(acl))
return &aclAuthorizer{acl: acl}, nil
}
@@ -151,6 +166,23 @@ func matchIP(ipp *string, ip net.IP) bool {
return ipnet.Contains(ip)
}
+func matchLabels(ml map[string]string, rl authn.Labels, vars []string) bool {
+ for label, pattern := range ml {
+ labelValues := rl[label]
+ matched := false
+ for _, lv := range labelValues {
+ if matchString(&pattern, lv, vars) {
+ matched = true
+ break
+ }
+ }
+ if !matched {
+ return false
+ }
+ }
+ return true
+}
+
var captureGroupRegex = regexp.MustCompile(`\$\{(.+?):(\d+)\}`)
func getField(i interface{}, name string) (string, bool) {
@@ -203,7 +235,8 @@ func (mc *MatchConditions) Matches(ai *AuthRequestInfo) bool {
return matchString(mc.Account, ai.Account, vars) &&
matchString(mc.Type, ai.Type, vars) &&
matchString(mc.Name, ai.Name, vars) &&
- matchIP(mc.IP, ai.IP)
+ matchIP(mc.IP, ai.IP) &&
+ matchLabels(mc.Labels, ai.Labels, vars)
}
func (e *ACLEntry) Matches(ai *AuthRequestInfo) bool {
diff --git a/auth_server/authz/acl_test.go b/auth_server/authz/acl_test.go
index 4b9cd753..51bb484d 100644
--- a/auth_server/authz/acl_test.go
+++ b/auth_server/authz/acl_test.go
@@ -29,6 +29,7 @@ func TestValidation(t *testing.T) {
{MatchConditions{IP: sp("192.168.0.0/16")}, true},
{MatchConditions{IP: sp("2001:db8::1")}, true},
{MatchConditions{IP: sp("2001:db8::/48")}, true},
+ {MatchConditions{Labels: map[string]string{"foo": "bar"}}, true},
// Invalid stuff
{MatchConditions{Account: sp("/foo?*/")}, false},
{MatchConditions{Type: sp("/foo?*/")}, false},
@@ -37,6 +38,7 @@ func TestValidation(t *testing.T) {
{MatchConditions{IP: sp("192.168.0.*")}, false},
{MatchConditions{IP: sp("foo")}, false},
{MatchConditions{IP: sp("2001:db8::/222")}, false},
+ {MatchConditions{Labels: map[string]string{"foo": "/bar?*/"}}, false},
}
for i, c := range cases {
result := validateMatchConditions(&c.mc)
@@ -50,6 +52,8 @@ func TestValidation(t *testing.T) {
func TestMatching(t *testing.T) {
ai1 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz"}
+ ai2 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz",
+ Labels: map[string][]string{"group": []string{"admins", "VIP"}}}
cases := []struct {
mc MatchConditions
ai AuthRequestInfo
@@ -80,6 +84,14 @@ func TestMatching(t *testing.T) {
{MatchConditions{IP: sp("2001:db8::2")}, AuthRequestInfo{IP: net.ParseIP("2001:db8::1")}, false},
{MatchConditions{IP: sp("2001:db8::/48")}, AuthRequestInfo{IP: net.ParseIP("2001:db8::1")}, true},
{MatchConditions{IP: sp("2001:db8::/48")}, AuthRequestInfo{IP: net.ParseIP("2001:db8::2")}, true},
+ // Label matching
+ {MatchConditions{Labels: map[string]string{"foo": "bar"}}, ai1, false},
+ {MatchConditions{Labels: map[string]string{"foo": "bar"}}, ai2, false},
+ {MatchConditions{Labels: map[string]string{"group": "admins"}}, ai2, true},
+ {MatchConditions{Labels: map[string]string{"foo": "bar", "group": "admins"}}, ai2, false}, // "and" logic
+ {MatchConditions{Labels: map[string]string{"group": "VIP"}}, ai2, true},
+ {MatchConditions{Labels: map[string]string{"group": "a*"}}, ai2, true},
+ {MatchConditions{Labels: map[string]string{"group": "/(admins|VIP)/"}}, ai2, true},
}
for i, c := range cases {
if result := c.mc.Matches(&c.ai); result != c.matches {
diff --git a/auth_server/authz/authz.go b/auth_server/authz/authz.go
index 3800ed42..53eba0e0 100644
--- a/auth_server/authz/authz.go
+++ b/auth_server/authz/authz.go
@@ -5,6 +5,8 @@ import (
"fmt"
"net"
"strings"
+
+ "github.com/cesanta/docker_auth/auth_server/authn"
)
// Authorizer interface performs authorization of the request.
@@ -39,6 +41,7 @@ type AuthRequestInfo struct {
Service string
IP net.IP
Actions []string
+ Labels authn.Labels
}
func (ai AuthRequestInfo) String() string {
diff --git a/auth_server/main.go b/auth_server/main.go
index caded573..ad70282f 100644
--- a/auth_server/main.go
+++ b/auth_server/main.go
@@ -143,13 +143,13 @@ func (rs *RestartableServer) WatchConfig() {
}
func (rs *RestartableServer) MaybeRestart() {
- glog.Infof("Restarting server")
+ glog.Infof("Validating new config")
c, err := server.LoadConfig(rs.configFile)
if err != nil {
glog.Errorf("Failed to reload config (server not restarted): %s", err)
return
}
- glog.Infof("New config loaded")
+ glog.Infof("Config ok, restarting server")
rs.hs.Stop()
rs.authServer.Stop()
rs.authServer, rs.hs = ServeOnce(c, rs.configFile, rs.hd)
diff --git a/auth_server/server/config.go b/auth_server/server/config.go
index e79aa3e1..115d2b48 100644
--- a/auth_server/server/config.go
+++ b/auth_server/server/config.go
@@ -124,6 +124,10 @@ func validate(c *Config) error {
}
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")
+ } else {
+ if err := authz.ValidateACL(c.ACL); err != nil {
+ return fmt.Errorf("invalid ACL: %s", err)
+ }
}
if c.ACLMongo != nil {
if err := c.ACLMongo.Validate("acl_mongo"); err != nil {
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 543c392a..3432a9e4 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -108,6 +108,7 @@ type authRequest struct {
Account string
Service string
Scopes []authScope
+ Labels authn.Labels
}
type authScope struct {
@@ -192,26 +193,26 @@ func (as *AuthServer) ParseRequest(req *http.Request) (*authRequest, error) {
return ar, nil
}
-func (as *AuthServer) Authenticate(ar *authRequest) (bool, error) {
+func (as *AuthServer) Authenticate(ar *authRequest) (bool, authn.Labels, error) {
for i, a := range as.authenticators {
- result, err := a.Authenticate(ar.Account, ar.Password)
- glog.V(2).Infof("Authn %s %s -> %t, %v", a.Name(), ar.Account, result, err)
+ result, labels, err := a.Authenticate(ar.Account, ar.Password)
+ glog.V(2).Infof("Authn %s %s -> %t, %+v, %v", a.Name(), ar.Account, result, labels, err)
if err != nil {
if err == authn.NoMatch {
continue
} else if err == authn.WrongPass {
glog.Warningf("Failed authentication with %s: %s", err)
- return false, nil
+ return false, nil, nil
}
err = fmt.Errorf("authn #%d returned error: %s", i+1, err)
glog.Errorf("%s: %s", ar, err)
- return false, err
+ return false, nil, err
}
- return result, nil
+ return result, labels, nil
}
// Deny by default.
glog.Warningf("%s did not match any authn rule", ar)
- return false, nil
+ return false, nil, nil
}
func (as *AuthServer) authorizeScope(ai *authz.AuthRequestInfo) ([]string, error) {
@@ -243,6 +244,7 @@ func (as *AuthServer) Authorize(ar *authRequest) ([]authzResult, error) {
Service: ar.Service,
IP: ar.RemoteIP,
Actions: scope.Actions,
+ Labels: ar.Labels,
}
actions, err := as.authorizeScope(ai)
if err != nil {
@@ -306,7 +308,7 @@ func (as *AuthServer) CreateToken(ar *authRequest, ares []authzResult) (string,
if err != nil || sigAlg2 != sigAlg {
return "", fmt.Errorf("failed to sign token: %s", err)
}
- glog.Infof("New token for %s: %s", *ar, claimsJSON)
+ glog.Infof("New token for %s %+v: %s", *ar, ar.Labels, claimsJSON)
return fmt.Sprintf("%s%s%s", payload, token.TokenSeparator, joseBase64UrlEncode(sig)), nil
}
@@ -349,7 +351,7 @@ func (as *AuthServer) doAuth(rw http.ResponseWriter, req *http.Request) {
}
glog.V(2).Infof("Auth request: %+v", ar)
{
- authnResult, err := as.Authenticate(ar)
+ authnResult, labels, err := as.Authenticate(ar)
if err != nil {
http.Error(rw, fmt.Sprintf("Authentication failed (%s)", err), http.StatusInternalServerError)
return
@@ -359,6 +361,7 @@ func (as *AuthServer) doAuth(rw http.ResponseWriter, req *http.Request) {
http.Error(rw, "Auth failed.", http.StatusUnauthorized)
return
}
+ ar.Labels = labels
}
if len(ar.Scopes) > 0 {
ares, err = as.Authorize(ar)
From f314b1b0d93913a0eb4b2dc80b8692daa9e85e10 Mon Sep 17 00:00:00 2001
From: ceecko
Date: Sun, 30 Oct 2016 01:28:57 +0200
Subject: [PATCH 046/209] Support for external authorization (#143)
* Support for external authorization
* Renamed ACLExt to ExtAuthz
---
auth_server/authz/ext_authz.go | 99 ++++++++++++++++++++++++++++++++++
auth_server/server/config.go | 12 ++++-
auth_server/server/server.go | 4 ++
examples/reference.yml | 7 +++
4 files changed, 120 insertions(+), 2 deletions(-)
create mode 100644 auth_server/authz/ext_authz.go
diff --git a/auth_server/authz/ext_authz.go b/auth_server/authz/ext_authz.go
new file mode 100644
index 00000000..fdf00316
--- /dev/null
+++ b/auth_server/authz/ext_authz.go
@@ -0,0 +1,99 @@
+/*
+ Copyright 2016 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 authz
+
+import (
+ "encoding/json"
+ "fmt"
+ "os/exec"
+ "strings"
+ "syscall"
+
+ "github.com/golang/glog"
+)
+
+type ExtAuthzConfig struct {
+ Command string `yaml:"command"`
+ Args []string `yaml:"args"`
+}
+
+type ExtAuthzStatus int
+
+const (
+ ExtAuthzAllowed ExtAuthzStatus = 0
+ ExtAuthzDenied ExtAuthzStatus = 1
+ ExtAuthzError ExtAuthzStatus = 2
+)
+
+func (c *ExtAuthzConfig) Validate() error {
+ if c.Command == "" {
+ return fmt.Errorf("command is not set")
+ }
+ if _, err := exec.LookPath(c.Command); err != nil {
+ return fmt.Errorf("invalid command %q: %s", c.Command, err)
+ }
+ return nil
+}
+
+type ExtAuthz struct {
+ cfg *ExtAuthzConfig
+}
+
+func NewExtAuthzAuthorizer(cfg *ExtAuthzConfig) *ExtAuthz {
+ glog.Infof("External authorization: %s %s", cfg.Command, strings.Join(cfg.Args, " "))
+ return &ExtAuthz{cfg: cfg}
+}
+
+func (ea *ExtAuthz) Authorize(ai *AuthRequestInfo) ([]string, error) {
+ aiMarshal, err := json.Marshal(ai)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to json.Marshal AuthRequestInfo: %s", err)
+ }
+
+ cmd := exec.Command(ea.cfg.Command, ea.cfg.Args...)
+ cmd.Stdin = strings.NewReader(fmt.Sprintf("%s", aiMarshal))
+ output, err := cmd.Output()
+
+ es := 0
+ et := ""
+ if err == nil {
+ } else if ee, ok := err.(*exec.ExitError); ok {
+ es = ee.Sys().(syscall.WaitStatus).ExitStatus()
+ et = string(ee.Stderr)
+ } else {
+ es = int(ExtAuthzError)
+ et = fmt.Sprintf("cmd run error: %s", err)
+ }
+ glog.V(2).Infof("%s %s -> %d %s", cmd.Path, cmd.Args, es, output)
+
+ switch ExtAuthzStatus(es) {
+ case ExtAuthzAllowed:
+ return ai.Actions, nil
+ case ExtAuthzDenied:
+ return []string{}, nil
+ default:
+ glog.Errorf("Ext command error: %d %s", es, et)
+ }
+ return nil, fmt.Errorf("bad return code from command: %d", es)
+}
+
+func (sua *ExtAuthz) Stop() {
+}
+
+func (sua *ExtAuthz) Name() string {
+ return "external authz"
+}
diff --git a/auth_server/server/config.go b/auth_server/server/config.go
index e0fbdd25..f6e53092 100644
--- a/auth_server/server/config.go
+++ b/auth_server/server/config.go
@@ -42,6 +42,7 @@ type Config struct {
ExtAuth *authn.ExtAuthConfig `yaml:"ext_auth,omitempty"`
ACL authz.ACL `yaml:"acl,omitempty"`
ACLMongo *authz.ACLMongoConfig `yaml:"acl_mongo,omitempty"`
+ ExtAuthz *authz.ExtAuthzConfig `yaml:"ext_authz,omitempty"`
}
type ServerConfig struct {
@@ -123,9 +124,11 @@ func validate(c *Config) error {
return fmt.Errorf("bad ext_auth config: %s", err)
}
}
- if c.ACL == nil && c.ACLMongo == nil {
+ if c.ACL == nil && c.ACLMongo == nil && c.ExtAuthz == nil {
return errors.New("ACL is empty, this is probably a mistake. Use an empty list if you really want to deny all actions")
- } else {
+ }
+
+ if c.ACL != nil {
if err := authz.ValidateACL(c.ACL); err != nil {
return fmt.Errorf("invalid ACL: %s", err)
}
@@ -135,6 +138,11 @@ func validate(c *Config) error {
return err
}
}
+ if c.ExtAuthz != nil {
+ if err := c.ExtAuthz.Validate(); err != nil {
+ return err
+ }
+ }
return nil
}
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 1e918793..b06ff02c 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -60,6 +60,10 @@ func NewAuthServer(c *Config) (*AuthServer, error) {
}
as.authorizers = append(as.authorizers, mongoAuthorizer)
}
+ if c.ExtAuthz != nil {
+ extAuthorizer := authz.NewExtAuthzAuthorizer(c.ExtAuthz)
+ as.authorizers = append(as.authorizers, extAuthorizer)
+ }
if c.Users != nil {
as.authenticators = append(as.authenticators, authn.NewStaticUserAuth(c.Users))
}
diff --git a/examples/reference.yml b/examples/reference.yml
index 220e6298..152f638a 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -225,3 +225,10 @@ acl_mongo:
# the MongoDB server.
# (See https://golang.org/pkg/time/#ParseDuration for a format description.)
cache_ttl: "1m"
+
+# External authorization - call an external progam to authorize user.
+# JSON of authz.AuthRequestInfo is passed to command's stdin and exit code is examined.
+# 0 - allow, 1 - deny, other - error.
+ext_authz:
+ command: "/usr/local/bin/my_authz" # Can be a relative path too; $PATH works.
+ args: ["--flag", "--more", "--flags"]
From 8158392bd421d50cfe06f289732cf13ce52db818 Mon Sep 17 00:00:00 2001
From: rojer
Date: Mon, 31 Oct 2016 13:30:42 +0000
Subject: [PATCH 047/209] Allow empty scope (#147)
Fixes #146
---
auth_server/server/server.go | 40 +++++++++++++++++++-----------------
1 file changed, 21 insertions(+), 19 deletions(-)
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index b06ff02c..3ddd3ecc 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -182,27 +182,29 @@ func (as *AuthServer) ParseRequest(req *http.Request) (*authRequest, error) {
return nil, fmt.Errorf("invalid form value")
}
// https://github.com/docker/distribution/blob/1b9ab303a477ded9bdd3fc97e9119fa8f9e58fca/docs/spec/auth/scope.md#resource-scope-grammar
- for _, scopeStr := range req.Form["scope"] {
- parts := strings.Split(scopeStr, ":")
- var scope authScope
- switch len(parts) {
- case 3:
- scope = authScope{
- Type: parts[0],
- Name: parts[1],
- Actions: strings.Split(parts[2], ","),
+ if req.FormValue("scope") != "" {
+ for _, scopeStr := range req.Form["scope"] {
+ parts := strings.Split(scopeStr, ":")
+ var scope authScope
+ switch len(parts) {
+ case 3:
+ scope = authScope{
+ Type: parts[0],
+ Name: parts[1],
+ Actions: strings.Split(parts[2], ","),
+ }
+ case 4:
+ scope = authScope{
+ Type: parts[0],
+ Name: parts[1] + ":" + parts[2],
+ Actions: strings.Split(parts[3], ","),
+ }
+ default:
+ return nil, fmt.Errorf("invalid scope: %q", scopeStr)
}
- case 4:
- scope = authScope{
- Type: parts[0],
- Name: parts[1] + ":" + parts[2],
- Actions: strings.Split(parts[3], ","),
- }
- default:
- return nil, fmt.Errorf("invalid scope: %q", scopeStr)
+ sort.Strings(scope.Actions)
+ ar.Scopes = append(ar.Scopes, scope)
}
- sort.Strings(scope.Actions)
- ar.Scopes = append(ar.Scopes, scope)
}
return ar, nil
}
From 0b4bb77de0ebbed40dbad06ab876d1a4d218915b Mon Sep 17 00:00:00 2001
From: rojer
Date: Mon, 31 Oct 2016 13:47:26 +0000
Subject: [PATCH 048/209] Update bindata; add -nocompress to avoid diffs (#148)
---
auth_server/authn/authn.go | 2 +-
auth_server/authn/bindata.go | 134 ++++++++++++++++++++++++++---------
2 files changed, 101 insertions(+), 35 deletions(-)
diff --git a/auth_server/authn/authn.go b/auth_server/authn/authn.go
index bdb17afa..70e56e82 100644
--- a/auth_server/authn/authn.go
+++ b/auth_server/authn/authn.go
@@ -42,7 +42,7 @@ type Authenticator interface {
var NoMatch = errors.New("did not match any rule")
var WrongPass = errors.New("wrong password for user")
-//go:generate go-bindata -pkg authn -modtime 1 -mode 420 data/
+//go:generate go-bindata -pkg authn -modtime 1 -mode 420 -nocompress data/
type PasswordString string
diff --git a/auth_server/authn/bindata.go b/auth_server/authn/bindata.go
index 4e9b5464..08b0104b 100644
--- a/auth_server/authn/bindata.go
+++ b/auth_server/authn/bindata.go
@@ -7,37 +7,13 @@
package authn
import (
- "bytes"
- "compress/gzip"
"fmt"
- "io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)
-
-func bindataRead(data []byte, name string) ([]byte, error) {
- gz, err := gzip.NewReader(bytes.NewBuffer(data))
- if err != nil {
- return nil, fmt.Errorf("Read %q: %v", name, err)
- }
-
- var buf bytes.Buffer
- _, err = io.Copy(&buf, gz)
- clErr := gz.Close()
-
- if err != nil {
- return nil, fmt.Errorf("Read %q: %v", name, err)
- }
- if clErr != nil {
- return nil, err
- }
-
- return buf.Bytes(), nil
-}
-
type asset struct {
bytes []byte
info os.FileInfo
@@ -69,13 +45,16 @@ func (fi bindataFileInfo) Sys() interface{} {
return nil
}
-var _dataGithub_authTmpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xac\x90\xb1\x6e\xf4\x20\x10\x84\x7b\x3f\x05\xa2\xf8\xaf\x33\xfd\x09\xfc\x2b\x4a\x91\x44\x4a\x95\x17\x88\x30\xde\x98\xd5\x61\x16\xc1\x92\xe8\x72\xba\x77\x0f\xc6\x45\x5e\x20\x0d\xda\x91\x98\x6f\x46\xa3\x3d\x6f\x41\x20\xc3\x56\x1c\x25\xe8\x17\x5f\x13\x18\xe9\x99\xd3\x59\xa9\xe2\x3c\x6c\x76\xa4\xbc\xaa\x87\xcc\xe8\x02\xc8\x69\xd0\x33\x2d\xd7\x69\x10\x42\xcf\x95\x99\xa2\x38\x2c\x87\x90\x82\xa2\x0b\xe8\x2e\x46\x06\x72\x96\x91\xe2\xe8\x33\x7c\x98\xd3\x8e\x2c\x8d\xb9\x22\xfb\x3a\x8f\x8e\x36\x15\x68\xc5\xa8\xc8\x56\xf6\x6a\x7f\x28\xe3\x37\xfc\xef\x5d\x4c\x2d\x90\xcf\x2d\x1c\xc3\xbf\xc6\x83\xc8\xef\xb8\x98\xdb\x6d\x7c\xec\xe2\x65\xb9\xdf\x4f\x72\x7a\xdd\x01\xe2\xab\x11\xc5\x13\xf2\x73\x9d\xb5\x3a\x6a\xfc\x49\xbd\x02\xcc\x18\xd7\xa2\x6c\x4a\xcd\xd3\x7f\x97\x96\xfa\x06\x9f\x74\x01\x61\x9d\x83\x52\x7e\x13\xdb\xd5\x87\xd1\x6a\x9f\x75\x1a\x7e\x02\x00\x00\xff\xff\x70\x1a\x30\x49\x5e\x01\x00\x00")
+var _dataGithub_authTmpl = []byte(`
+
+
+
+
+
+`)
func dataGithub_authTmplBytes() ([]byte, error) {
- return bindataRead(
- _dataGithub_authTmpl,
- "data/github_auth.tmpl",
- )
+ return _dataGithub_authTmpl, nil
}
func dataGithub_authTmpl() (*asset, error) {
@@ -89,13 +68,100 @@ func dataGithub_authTmpl() (*asset, error) {
return a, nil
}
-var _dataGoogle_authTmpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xdc\x56\x6d\x6b\xeb\x36\x14\xfe\x9c\xfc\x0a\xe1\x5d\xb0\xc3\x7a\x65\x76\x3f\x8c\x4b\x6e\x92\xd1\xdd\xc1\xe8\x18\x6b\x59\xbb\x4f\x63\x04\x55\x3e\x76\xd4\x2a\x92\x27\x1d\x77\x0d\x21\xff\x7d\x47\x92\xf3\xd2\x97\x84\x0e\xc6\xc6\x16\x68\x6d\x9f\xa3\xf3\x9c\x17\x3d\x8f\xe5\xc9\x02\x97\x9a\x29\x84\xa5\x97\xb6\x85\x78\x87\xab\x16\xa6\xd9\x02\xb1\x1d\x97\xa5\x97\x0b\x58\x0a\x6e\x5d\x53\x9e\x3b\x54\x52\x43\x36\x1b\x4e\x16\x20\xaa\xd9\x90\xb1\x89\x97\x4e\xb5\xc8\xbc\x93\xd3\xac\x2c\xc5\x9d\x78\xe4\x8d\xb5\x8d\x06\xd1\x2a\xcf\xa5\x5d\x46\x5b\xa9\xd5\xad\x2f\xef\x7e\xef\xc0\xad\xca\xaf\xf8\x47\xfe\xa1\x7f\xe0\x4b\x65\xf8\x9d\xcf\x66\x93\x32\x21\xbd\x00\x0d\x65\x78\xaa\x23\xe2\x25\xe8\x08\x7b\xe7\x4b\xa9\x15\x18\x1c\xb7\x5a\x60\x6d\xdd\x92\x70\xbe\xb1\x46\x5b\x51\x4d\x3d\x0a\x87\x19\x13\x7e\x65\x24\xab\xa0\x06\xf7\x5a\x82\xd9\x70\x30\xa8\x3b\x23\x51\x59\xc3\xa8\x4d\x79\xff\xa3\x6d\x94\x29\x46\x6c\x4d\x9e\xc1\x83\x70\x4c\x74\xb8\xf8\xc0\xa6\xac\xa1\xf4\x3c\x3e\xf0\x06\xf0\x9c\x6e\x2e\x0c\x25\x31\x12\x8a\xd1\xa7\xb0\x58\xd5\xac\x48\x7e\xe5\xaf\x55\x63\xa0\xba\x30\x61\x69\x31\xea\xd1\x06\xef\x8a\xfc\x0b\x07\xbe\xd3\x98\x8f\x38\xc2\x23\x16\xf9\x83\xd0\xaa\x12\xa8\x4c\xc3\xe0\x51\xf9\x78\x83\xf6\x1e\x0c\xe7\x3c\x4f\xb8\xb1\x0a\x55\xcd\xa3\x99\x0a\x49\x39\x64\xe7\x1c\xb5\xfe\x8b\x07\x97\x92\x6c\xab\xfa\x19\x7c\x6b\x8d\xa7\xaa\xf8\x36\x28\xc1\xbc\xe3\x61\x1f\x8a\x54\xca\x20\xec\xf0\x98\xe5\x57\x97\xd7\x37\xf9\x59\x32\x75\x4e\x93\xa5\x4c\x13\x9e\x87\x34\x5b\x8f\xb4\x06\x29\xd9\x4d\x8a\x11\x6d\xab\x95\x14\x61\x66\xb4\x07\xd6\x7c\xa2\xc9\x09\xe7\x01\xa7\x1d\xd6\xef\x3f\x6e\x83\x5a\x67\x25\x78\xff\x9d\x40\x31\x66\xb5\xd0\x1e\x7a\x47\x15\x2d\x3f\x5c\x5f\xfe\xc4\x3d\x3a\xea\x58\xd5\xab\x62\x9d\x8b\xb8\x0b\x39\x25\x88\x1b\x91\x9f\xb1\x3c\x56\x4f\x96\x6d\x23\x9b\x51\x0f\xe1\x3b\x19\xb0\x09\xb7\xdf\xbc\x22\xcd\x75\x3b\xe9\xd7\x66\xdd\xaf\x48\xc3\x18\x6c\x7a\x24\x70\xce\xba\x03\x9c\xc7\x85\x3b\x05\x92\xf7\xeb\x73\xf6\x25\xa3\xa5\xdc\xf5\xd3\xbe\x21\xe7\x33\xe8\x4d\x7a\xde\x30\xa0\xd6\x7b\x48\x1a\xa4\xb7\x44\x5f\x6d\x9b\x22\x37\x16\x19\xdd\x34\x50\x31\x65\xfa\xcd\xde\x0c\xc3\x1f\xa3\xdf\x8e\x96\x91\xc9\x91\x91\x2c\xfe\x22\x11\x03\xc7\x8b\x3c\x52\x81\x06\xb5\xab\x7e\x5b\xfa\x01\x59\x95\x51\x58\xac\x93\x50\xe6\xaa\xa2\xd2\xd7\x6b\xfe\x39\x3e\x5e\x54\x9b\x4d\xbe\xa1\xce\x16\x60\x8a\x3d\xfb\xfb\x4a\xe8\x12\xd2\x85\x6a\xf6\xca\x99\x94\x49\xf7\x93\x5b\x5b\xad\x48\x3d\x93\xdb\x0e\x91\x8a\x54\xd5\x34\xf3\x44\x7a\x65\xbe\x8d\x86\x6c\x16\x24\x40\x7d\xb1\x3f\x14\x2e\xd8\xf7\x91\x53\x93\x32\xad\x0e\x71\x7b\xfd\x85\x21\x1f\x86\xd2\xa8\xa9\x5a\x79\x5f\x3c\xef\xaa\x2c\x59\x58\x77\x61\x3e\x0b\xad\x6f\x85\xbc\x0f\xba\x56\x26\x8e\x8f\xa6\x04\x2d\xfb\x9a\xff\x65\xd5\xf6\x3e\x27\x0c\x5e\xd6\xb5\x26\xb8\xf3\x48\x2d\x22\xa4\x83\x4a\x39\x90\x38\xef\x9c\x0a\xb4\x6c\xad\xc7\x25\xb9\x44\x03\x81\x9c\xc4\xef\x65\x8b\x91\xaf\x81\x01\x06\x77\xa3\xdc\x15\x2e\x92\x1e\x0f\x88\x79\x48\x80\x03\xef\x7f\x4d\xa1\x61\x1f\xe6\x44\xda\xb3\xd0\x7c\x05\x64\xda\x37\xf3\x6b\x32\xfd\xf6\x37\x8a\xf5\xc9\xd8\xb2\xe4\x1b\x67\x67\xec\x1f\x93\xf4\x53\xe1\xa6\x00\x6a\xfe\x8d\x6f\x80\x78\x89\xff\xf7\x42\x7a\xa1\x9c\xcb\x0e\x9f\x48\xc7\x76\x78\x5a\x2f\xbb\x80\xa3\x82\xf9\x5f\x1d\x5f\xa4\xfe\x2b\x70\xe1\x94\x67\x14\xf4\x00\xee\xbd\x57\x15\xc4\x37\x42\x98\x15\xff\x57\x14\xf4\x36\xa1\x50\x79\x27\x4f\xb3\xa3\x3a\x7c\xa9\x1c\xda\x8b\x63\x34\x3f\x70\xd1\xd7\x51\xab\x01\xe1\x98\x08\x9e\xb0\x79\x3b\xc1\x5e\x4d\xa7\x78\x3d\x38\xc5\x22\x5e\x29\xfa\x80\x34\x86\xde\x98\xc5\x2e\xe0\x15\xce\xf8\x48\xae\x90\x31\x7f\xc3\x89\xf9\x0a\xc0\xd1\x73\xf3\xb9\xc2\x2a\xf5\x10\xe5\x95\xe2\xc3\x17\x26\x59\xc2\x19\x96\x0e\x2f\x3a\xcb\xe8\xcb\x77\x36\xfc\x33\x00\x00\xff\xff\x8d\x6b\x3d\x03\x01\x0b\x00\x00")
+var _dataGoogle_authTmpl = []byte(`
+
+
+
+
+
+
+
+
+
+
+
+
+
+`)
func dataGoogle_authTmplBytes() ([]byte, error) {
- return bindataRead(
- _dataGoogle_authTmpl,
- "data/google_auth.tmpl",
- )
+ return _dataGoogle_authTmpl, nil
}
func dataGoogle_authTmpl() (*asset, error) {
From b5b63d933e370be6abdd0b4744af1279159a69ea Mon Sep 17 00:00:00 2001
From: rojer
Date: Mon, 31 Oct 2016 22:48:40 +0000
Subject: [PATCH 049/209] Document auth labels, add an example for ext_auth
---
examples/ext_auth.sh | 17 +++++++++++++++++
examples/reference.yml | 3 +++
2 files changed, 20 insertions(+)
create mode 100755 examples/ext_auth.sh
diff --git a/examples/ext_auth.sh b/examples/ext_auth.sh
new file mode 100755
index 00000000..e5a17063
--- /dev/null
+++ b/examples/ext_auth.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+#
+# Example external authenticator program for use with `ext_auth`.
+#
+
+read u p
+
+if [ "$u" == "user" -a "$p" == "pass" ]; then
+ exit 0
+fi
+
+if [ "$u" == "bofh" -a "$p" == "LART" ]; then
+ echo '{"labels": {"level": ["max"], "groups": ["VIP", "ATeam"]}}'
+ exit 0
+fi
+
+exit 1
diff --git a/examples/reference.yml b/examples/reference.yml
index 152f638a..8737bf60 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -133,6 +133,9 @@ mongo_auth:
# External authentication - call an external progam to authenticate user.
# Username and password are passed to command's stdin and exit code is examined.
# 0 - allow, 1 - deny, 2 - no match, other - error.
+# In case of success, if any output is returned, it is parsed as a JSON object.
+# The "labels" key may contain labels to be passed down to authz, where they can
+# be used in matching. See ext_auth.sh for an example.
ext_auth:
command: "/usr/local/bin/my_auth" # Can be a relative path too; $PATH works.
args: ["--flag", "--more", "--flags"]
From 3c31d7ad141451d87f3667ed06ce96793c321f41 Mon Sep 17 00:00:00 2001
From: Segev Finer
Date: Fri, 23 Dec 2016 13:32:28 +0200
Subject: [PATCH 050/209] ldap_auth: An empty password worked for any valid
user (#155)
See: https://github.com/go-ldap/ldap/issues/93
---
auth_server/authn/ldap_auth.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/auth_server/authn/ldap_auth.go b/auth_server/authn/ldap_auth.go
index a885b40c..38ec9dad 100644
--- a/auth_server/authn/ldap_auth.go
+++ b/auth_server/authn/ldap_auth.go
@@ -54,7 +54,7 @@ func NewLDAPAuth(c *LDAPAuthConfig) (*LDAPAuth, error) {
//How to authenticate user, please refer to https://github.com/go-ldap/ldap/blob/master/example_test.go#L166
func (la *LDAPAuth) Authenticate(account string, password PasswordString) (bool, Labels, error) {
- if account == "" {
+ if account == "" || password == "" {
return false, nil, NoMatch
}
l, err := la.ldapConnection()
From 1f6471f438d4efe440473e6b61f823e14ebe846f Mon Sep 17 00:00:00 2001
From: Jan Schmitz-Hermes
Date: Fri, 23 Dec 2016 19:09:08 +0100
Subject: [PATCH 051/209] Feature: GHE Support (#151)
* Parametrize GitHub host, for use with GHE
---
auth_server/authn/data/github_auth.tmpl | 4 ++--
auth_server/authn/github_auth.go | 25 ++++++++++++++++++++++---
examples/reference.yml | 6 ++++++
3 files changed, 30 insertions(+), 5 deletions(-)
diff --git a/auth_server/authn/data/github_auth.tmpl b/auth_server/authn/data/github_auth.tmpl
index 9fb86871..05daa798 100644
--- a/auth_server/authn/data/github_auth.tmpl
+++ b/auth_server/authn/data/github_auth.tmpl
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/auth_server/authn/github_auth.go b/auth_server/authn/github_auth.go
index 9bef12b2..49e58965 100644
--- a/auth_server/authn/github_auth.go
+++ b/auth_server/authn/github_auth.go
@@ -39,6 +39,8 @@ type GitHubAuthConfig struct {
TokenDB string `yaml:"token_db,omitempty"`
HTTPTimeout time.Duration `yaml:"http_timeout,omitempty"`
RevalidateAfter time.Duration `yaml:"revalidate_after,omitempty"`
+ GithubWebUri string `yaml:"github_web_uri,omitempty"`
+ GithubApiUri string `yaml:"github_api_uri,omitempty"`
}
type GitHubAuthRequest struct {
@@ -90,13 +92,30 @@ func (gha *GitHubAuth) DoGitHubAuth(rw http.ResponseWriter, req *http.Request) {
}
}
+func (gha *GitHubAuth) getGithubApiUri() string {
+ if gha.config.GithubApiUri != "" {
+ return gha.config.GithubApiUri
+ } else {
+ return "https://api.github.com/"
+ }
+}
+
+func (gha *GitHubAuth) getGithubWebUri() string {
+ if gha.config.GithubWebUri != "" {
+ return gha.config.GithubWebUri
+ } else {
+ return "https://github.com/"
+ }
+}
+
func (gha *GitHubAuth) doGitHubAuthCreateToken(rw http.ResponseWriter, code string) {
data := url.Values{
"code": []string{string(code)},
"client_id": []string{gha.config.ClientId},
"client_secret": []string{gha.config.ClientSecret},
}
- req, err := http.NewRequest("POST", "https://github.com/login/oauth/access_token", bytes.NewBufferString(data.Encode()))
+
+ req, err := http.NewRequest("POST", fmt.Sprintf("%s/login/oauth/access_token", gha.getGithubWebUri()), bytes.NewBufferString(data.Encode()))
if err != nil {
http.Error(rw, fmt.Sprintf("Error creating request to GitHub auth backend: %s", err), http.StatusServiceUnavailable)
return
@@ -150,7 +169,7 @@ func (gha *GitHubAuth) doGitHubAuthCreateToken(rw http.ResponseWriter, code stri
}
func (gha *GitHubAuth) validateAccessToken(token string) (user string, err error) {
- req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
+ req, err := http.NewRequest("GET", fmt.Sprintf("%s/user", gha.getGithubApiUri()), nil)
if err != nil {
err = fmt.Errorf("could not create request to get information for token %s: %s", token, err)
return
@@ -187,7 +206,7 @@ func (gha *GitHubAuth) checkOrganization(token, user string) (err error) {
if gha.config.Organization == "" {
return nil
}
- url := fmt.Sprintf("https://api.github.com/orgs/%s/members/%s", gha.config.Organization, user)
+ url := fmt.Sprintf("%s/orgs/%s/members/%s", gha.getGithubApiUri(), gha.config.Organization, user)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
err = fmt.Errorf("could not create request to get organization membership: %s", err)
diff --git a/examples/reference.yml b/examples/reference.yml
index 8737bf60..716a8d01 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -87,6 +87,12 @@ github_auth:
http_timeout: "10s"
# How long to wait before revalidating the GitHub token. Optional.
revalidate_after: "1h"
+ # The Github Web URI in case you are using Github Enterprise.
+ # Includes the protocol, without trailing slash. Optional - defaults to: https://github.com
+ github_web_uri: "https://github.acme.com/"
+ # The Github API URI in case you are using Github Enterprise.
+ # Includes the protocol, without trailing slash. - defaults to: https://api.github.com
+ github_api_uri: "https://github.acme.com/api/v3"
# LDAP authentication.
# Authentication is performed by first binding to the server, looking up the user entry
From 99a73068f45feac016c874db8a1deb795b94ae02 Mon Sep 17 00:00:00 2001
From: rojer
Date: Fri, 23 Dec 2016 18:25:32 +0000
Subject: [PATCH 052/209] Update bindata
---
auth_server/authn/bindata.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/auth_server/authn/bindata.go b/auth_server/authn/bindata.go
index 08b0104b..66db1f55 100644
--- a/auth_server/authn/bindata.go
+++ b/auth_server/authn/bindata.go
@@ -47,8 +47,8 @@ func (fi bindataFileInfo) Sys() interface{} {
var _dataGithub_authTmpl = []byte(`
-
-
+
+
`)
@@ -63,7 +63,7 @@ func dataGithub_authTmpl() (*asset, error) {
return nil, err
}
- info := bindataFileInfo{name: "data/github_auth.tmpl", size: 350, mode: os.FileMode(420), modTime: time.Unix(1, 0)}
+ info := bindataFileInfo{name: "data/github_auth.tmpl", size: 348, mode: os.FileMode(420), modTime: time.Unix(1, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
From cfe79795a6fbb0365caba3ac4e3bbe4b1cf423f7 Mon Sep 17 00:00:00 2001
From: Webb Lu
Date: Wed, 4 Jan 2017 01:19:00 +0800
Subject: [PATCH 053/209] replace #!/bin/bash by #!/bin/sh, because bash not
exist in the :stable image (#159)
---
examples/ext_auth.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples/ext_auth.sh b/examples/ext_auth.sh
index e5a17063..ed434857 100755
--- a/examples/ext_auth.sh
+++ b/examples/ext_auth.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/bin/sh
#
# Example external authenticator program for use with `ext_auth`.
#
From 69c6eeefe96cb9fb1cc7fe44fdec576cdea9218f Mon Sep 17 00:00:00 2001
From: Marc MILLIEN
Date: Tue, 3 Jan 2017 18:19:40 +0100
Subject: [PATCH 054/209] Quick fix so user knows that he should set registry
fqdn (#158)
Ref.: https://github.com/cesanta/docker_auth/issues/157
Say `docker login YOUR_REGISTRY_FQDN` as a quick n dirty fix so the user knows that he should set its registry url.
---
auth_server/authn/github_auth.go | 2 +-
auth_server/authn/google_auth.go | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/auth_server/authn/github_auth.go b/auth_server/authn/github_auth.go
index 49e58965..3a3de558 100644
--- a/auth_server/authn/github_auth.go
+++ b/auth_server/authn/github_auth.go
@@ -165,7 +165,7 @@ func (gha *GitHubAuth) doGitHubAuthCreateToken(rw http.ResponseWriter, code stri
return
}
- fmt.Fprintf(rw, `Server logged in; now run "docker login", use %s as login and %s as password.`, user, dp)
+ fmt.Fprintf(rw, `Server logged in; now run "docker login YOUR_REGISTRY_FQDN", use %s as login and %s as password.`, user, dp)
}
func (gha *GitHubAuth) validateAccessToken(token string) (user string, err error) {
diff --git a/auth_server/authn/google_auth.go b/auth_server/authn/google_auth.go
index cd0704cb..93f1a94a 100644
--- a/auth_server/authn/google_auth.go
+++ b/auth_server/authn/google_auth.go
@@ -234,7 +234,7 @@ func (ga *GoogleAuth) doGoogleAuthCreateToken(rw http.ResponseWriter, code strin
return
}
- fmt.Fprintf(rw, `Server logged in; now run "docker login", use %s as login and %s as password.`, user, dp)
+ fmt.Fprintf(rw, `Server logged in; now run "docker login YOUR_REGISTRY_FQDN", use %s as login and %s as password.`, user, dp)
}
func (ga *GoogleAuth) getIDTokenInfo(token string) (*GoogleTokenInfo, error) {
From 7568942f021f0e2899de03975b28fcb715c23b5b Mon Sep 17 00:00:00 2001
From: rojer
Date: Thu, 26 Jan 2017 11:21:39 +0000
Subject: [PATCH 055/209] Update README
---
README.md | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 4fe0513a..f499c55f 100644
--- a/README.md
+++ b/README.md
@@ -15,15 +15,21 @@ Supported authentication methods:
* 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 ([demo](https://github.com/kwk/docker-registry-setup))
* MongoDB user collection
- * External program
+ * [External program](https://github.com/cesanta/docker_auth/blob/master/examples/ext_auth.sh)
Supported authorization methods:
* Static ACL
* MongoDB-backed ACL
+ * External program
## Installation and Examples
-A public Docker image is available on Docker Hub: [cesanta/docker_auth:stable](https://registry.hub.docker.com/u/cesanta/docker_auth/).
+A public Docker image is available on Docker Hub: [cesanta/docker](https://registry.hub.docker.com/u/cesanta/docker_auth/).
+
+Tags available:
+ - `:latest` - bleeding edge, usually works but breaking config changes are possible. You probably do not want to use this in production.
+ - `:1` - the `1.x` version, will have fixes, no breaking config changes. Previously known as `:stable`.
+ - `:1.x` - specific release, see [here](https://github.com/cesanta/docker_auth/releases) for the list of current releases.
The binary takes a single argument - path to the config file.
If no arguments are given, the Dockerfile defaults to `/config/auth_config.yml`.
@@ -35,7 +41,7 @@ $ docker run \
--rm -it --name docker_auth -p 5001:5001 \
-v /path/to/config_dir:/config:ro \
-v /var/log/docker_auth:/logs \
- cesanta/docker_auth:stable /config/auth_config.yml
+ cesanta/docker_auth:1 /config/auth_config.yml
```
See the [example config files](https://github.com/cesanta/docker_auth/tree/master/examples/) to get an idea of what is possible.
@@ -44,7 +50,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:stable --v=2 --alsologtostderr /config/auth_config.yml
+docker run ... cesanta/docker_auth:1 --v=2 --alsologtostderr /config/auth_config.yml
```
## Contributing
From 2cc197f58e4f28d1def55ce4824eea0f8675e323 Mon Sep 17 00:00:00 2001
From: Thatcher
Date: Mon, 30 Jan 2017 15:45:16 +0100
Subject: [PATCH 056/209] Fixed simple mistake in README (#164)
In text the link to the container was cesanta/docker, while it should (have) been cesanta/docker_auth
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index f499c55f..85813f79 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ Supported authorization methods:
## Installation and Examples
-A public Docker image is available on Docker Hub: [cesanta/docker](https://registry.hub.docker.com/u/cesanta/docker_auth/).
+A public Docker image is available on Docker Hub: [cesanta/docker_auth](https://registry.hub.docker.com/u/cesanta/docker_auth/).
Tags available:
- `:latest` - bleeding edge, usually works but breaking config changes are possible. You probably do not want to use this in production.
From 286369f8bf1d79c27c9f92f2b38d93511f4a7fe6 Mon Sep 17 00:00:00 2001
From: Dennis de Greef
Date: Mon, 27 Feb 2017 06:39:58 +0100
Subject: [PATCH 057/209] Add ServerName to tlsConfig when InsecureSkipVerify
is false (#165)
---
auth_server/authn/ldap_auth.go | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/auth_server/authn/ldap_auth.go b/auth_server/authn/ldap_auth.go
index 38ec9dad..f8fc08ff 100644
--- a/auth_server/authn/ldap_auth.go
+++ b/auth_server/authn/ldap_auth.go
@@ -136,18 +136,25 @@ func (la *LDAPAuth) escapeAccountInput(account string) string {
func (la *LDAPAuth) ldapConnection() (*ldap.Conn, error) {
var l *ldap.Conn
var err error
+
+ tlsConfig := &tls.Config{InsecureSkipVerify: true}
+ if !la.config.InsecureTLSSkipVerify {
+ addr := strings.Split(la.config.Addr, ":")
+ tlsConfig = &tls.Config{InsecureSkipVerify: false, ServerName: addr[0]}
+ }
+
if la.config.TLS == "" || la.config.TLS == "none" || la.config.TLS == "starttls" {
glog.V(2).Infof("Dial: starting...%s", la.config.Addr)
l, err = ldap.Dial("tcp", fmt.Sprintf("%s", la.config.Addr))
if err == nil && la.config.TLS == "starttls" {
glog.V(2).Infof("StartTLS...")
- if tlserr := l.StartTLS(&tls.Config{InsecureSkipVerify: la.config.InsecureTLSSkipVerify}); tlserr != nil {
+ if tlserr := l.StartTLS(tlsConfig); tlserr != nil {
return nil, tlserr
}
}
} else if la.config.TLS == "always" {
glog.V(2).Infof("DialTLS: starting...%s", la.config.Addr)
- l, err = ldap.DialTLS("tcp", fmt.Sprintf("%s", la.config.Addr), &tls.Config{InsecureSkipVerify: la.config.InsecureTLSSkipVerify})
+ l, err = ldap.DialTLS("tcp", fmt.Sprintf("%s", la.config.Addr), tlsConfig)
}
if err != nil {
return nil, err
From f298f05ef75b5e5f3f7f60f99c4e6baaf631538d Mon Sep 17 00:00:00 2001
From: rojer
Date: Mon, 3 Apr 2017 20:11:17 +0100
Subject: [PATCH 058/209] GitHub auth: Add GithubWebUri to the template context
Fixes #169
---
auth_server/authn/github_auth.go | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/auth_server/authn/github_auth.go b/auth_server/authn/github_auth.go
index 3a3de558..7e1a9445 100644
--- a/auth_server/authn/github_auth.go
+++ b/auth_server/authn/github_auth.go
@@ -76,7 +76,11 @@ func NewGitHubAuth(c *GitHubAuthConfig) (*GitHubAuth, error) {
}
func (gha *GitHubAuth) doGitHubAuthPage(rw http.ResponseWriter, req *http.Request) {
- if err := gha.tmpl.Execute(rw, struct{ ClientId string }{ClientId: gha.config.ClientId}); err != nil {
+ if err := gha.tmpl.Execute(rw, struct {
+ ClientId, GithubWebUri string
+ }{
+ ClientId: gha.config.ClientId,
+ GithubWebUri: gha.getGithubWebUri()}); err != nil {
http.Error(rw, fmt.Sprintf("Template error: %s", err), http.StatusInternalServerError)
}
}
@@ -114,7 +118,7 @@ func (gha *GitHubAuth) doGitHubAuthCreateToken(rw http.ResponseWriter, code stri
"client_id": []string{gha.config.ClientId},
"client_secret": []string{gha.config.ClientSecret},
}
-
+
req, err := http.NewRequest("POST", fmt.Sprintf("%s/login/oauth/access_token", gha.getGithubWebUri()), bytes.NewBufferString(data.Encode()))
if err != nil {
http.Error(rw, fmt.Sprintf("Error creating request to GitHub auth backend: %s", err), http.StatusServiceUnavailable)
From 80d8a93b116c319a705ac73ab408d9471b596fdf Mon Sep 17 00:00:00 2001
From: rojer
Date: Mon, 3 Apr 2017 20:25:30 +0100
Subject: [PATCH 059/209] Fix parsing of IPv6 RemoteAddr
Fixes #168
---
auth_server/server/server.go | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 3ddd3ecc..6f4de2f2 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -23,6 +23,7 @@ import (
"math/rand"
"net"
"net/http"
+ "regexp"
"sort"
"strings"
"time"
@@ -33,6 +34,10 @@ import (
"github.com/golang/glog"
)
+var (
+ hostPortRegex = regexp.MustCompile(`\[?(.+?)\]?:\d+$`)
+)
+
type AuthServer struct {
config *Config
authenticators []authn.Authenticator
@@ -131,12 +136,9 @@ func (ar authRequest) String() string {
}
func parseRemoteAddr(ra string) net.IP {
- colonIndex := strings.LastIndex(ra, ":")
- if colonIndex > 0 && ra[colonIndex-1] >= 0x30 && ra[colonIndex-1] <= 0x39 {
- ra = ra[:colonIndex]
- }
- if ra[0] == '[' && ra[len(ra)-1] == ']' { // IPv6
- ra = ra[1 : len(ra)-1]
+ hp := hostPortRegex.FindStringSubmatch(ra)
+ if hp != nil {
+ ra = string(hp[1])
}
res := net.ParseIP(ra)
return res
From e37561bf234aeb2295bdec9a1307f2d1f6036167 Mon Sep 17 00:00:00 2001
From: rojer
Date: Mon, 3 Apr 2017 20:44:18 +0100
Subject: [PATCH 060/209] Send WWW-Authenticate header with 401 response
Not sure what difference it'll make, but shouldn't hurt.
Fixes #152
---
auth_server/server/server.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 6f4de2f2..be5a2947 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -376,6 +376,7 @@ func (as *AuthServer) doAuth(rw http.ResponseWriter, req *http.Request) {
}
if !authnResult {
glog.Warningf("Auth failed: %s", *ar)
+ rw.Header()["WWW-Authenticate"] = []string{fmt.Sprintf(`Basic realm="%s"`, as.config.Token.Issuer)}
http.Error(rw, "Auth failed.", http.StatusUnauthorized)
return
}
From 917428ac38d7c034d4e1ed7e13f0fd650c82b368 Mon Sep 17 00:00:00 2001
From: rojer
Date: Tue, 4 Apr 2017 01:44:48 +0100
Subject: [PATCH 061/209] Revamp the build process to incorporate govendor
Use the official golang base image for release builds
Fixes #150
---
auth_server/.gitignore | 2 +-
auth_server/Makefile | 48 ++--
auth_server/vendor/vendor.json | 449 +++++++++++++++++++++++++++++++++
3 files changed, 478 insertions(+), 21 deletions(-)
create mode 100644 auth_server/vendor/vendor.json
diff --git a/auth_server/.gitignore b/auth_server/.gitignore
index ea8e804c..e63dae2a 100644
--- a/auth_server/.gitignore
+++ b/auth_server/.gitignore
@@ -1,4 +1,4 @@
ca-certificates.crt
auth_server
-Godeps/
+vendor/*/
version.*
diff --git a/auth_server/Makefile b/auth_server/Makefile
index f716b448..25369812 100644
--- a/auth_server/Makefile
+++ b/auth_server/Makefile
@@ -4,47 +4,55 @@ COMPRESS_BINARY ?= false
CA_BUNDLE = /etc/ssl/certs/ca-certificates.crt
VERSION = $(shell cat version.txt)
-BUILDER_IMAGE ?= centurylink/golang-builder
-BUILDER_IMAGE_EXTRA-build-cross = -cross
-BUILDER_OPTS-docker-build = -v /var/run/docker.sock:/var/run/docker.sock
-BUILDER_IMAGE_EXTRA-docker-build =
+BUILDER_IMAGE ?= golang:1.8.0-alpine
.PHONY: %
all: build
-local: build-local
-update-deps:
- go get -v -u -f github.com/tools/godep github.com/jteeuwen/go-bindata/...
- go generate ./...
-
-godep:
- godep save
+deps:
+ go get -v -u github.com/kardianos/govendor
+ govendor sync
+ go install -v github.com/cesanta/docker_auth/auth_server/vendor/github.com/jteeuwen/go-bindata/go-bindata
-build-local: update-deps
- go build
+build:
+ go generate ./...
+ go build -v -i
ca-certificates.crt:
cp $(CA_BUNDLE) .
-docker-build:
- go generate ./...
- docker run --rm -v $(PWD):/src -e COMPRESS_BINARY=$(COMPRESS_BINARY) $(BUILDER_OPTS-$@) $(BUILDER_IMAGE)$(BUILDER_IMAGE_EXTRA-$@) $(IMAGE)
+build-release: ca-certificates.crt
+ docker run --rm -v $(PWD)/..:/go/src/github.com/cesanta/docker_auth \
+ $(BUILDER_IMAGE) sh -x -c "\
+ apk update && apk add git py2-pip && pip install GitPython && \
+ cd /go/src/github.com/cesanta/docker_auth/auth_server && \
+ go get -v -u github.com/kardianos/govendor && \
+ umask 0 && govendor sync -v && \
+ go install -v github.com/cesanta/docker_auth/auth_server/vendor/github.com/jteeuwen/go-bindata/go-bindata && \
+ go generate ./... && \
+ go build -v"
@echo === Built version $(VERSION) ===
-build build-cross: update-deps godep ca-certificates.crt docker-build
+auth_server:
+ @echo
+ @echo Use build or build-release to produce the auth_server binary
+ @echo
+ @exit 1
-docker-tag:
+docker-build: auth_server
+ docker build -t $(IMAGE):latest .
docker tag $(IMAGE):latest $(IMAGE):$(VERSION)
docker-tag-%:
docker tag $(IMAGE):latest $(IMAGE):$*
docker-push:
+ docker push $(IMAGE):latest
docker push $(IMAGE):$(VERSION)
docker-push-%: docker-tag-%
docker push $(IMAGE):$*
-# Shortcut for latest
-docker-push: docker-push-latest
+clean:
+ rm -rf auth_server vendor/*/*
diff --git a/auth_server/vendor/vendor.json b/auth_server/vendor/vendor.json
new file mode 100644
index 00000000..e0f1ec28
--- /dev/null
+++ b/auth_server/vendor/vendor.json
@@ -0,0 +1,449 @@
+{
+ "comment": "",
+ "ignore": "",
+ "package": [
+ {
+ "checksumSHA1": "CujWu7+PWlZSX5+zAPJH91O5AVQ=",
+ "origin": "github.com/docker/distribution/vendor/github.com/Sirupsen/logrus",
+ "path": "github.com/Sirupsen/logrus",
+ "revision": "0700fa570d7bcc1b3e46ee127c4489fd25f4daa3",
+ "revisionTime": "2017-03-21T17:14:25Z"
+ },
+ {
+ "checksumSHA1": "drfK6NPoGhSk7ZD6uhL+JjEpKy4=",
+ "path": "github.com/dchest/uniuri",
+ "revision": "8902c56451e9b58ff940bbe5fec35d5f9c04584a",
+ "revisionTime": "2016-02-12T16:43:26Z"
+ },
+ {
+ "checksumSHA1": "slvmkxxMkd1KThsdCXp1cUH+/H0=",
+ "path": "github.com/deckarep/golang-set",
+ "revision": "fc8930a5e645572ee00bf66358ed3414f3c13b90",
+ "revisionTime": "2017-02-02T20:30:32Z"
+ },
+ {
+ "checksumSHA1": "rAbbq6Q42Svc1VuHABqk4Xin4vM=",
+ "path": "github.com/docker/distribution/context",
+ "revision": "0700fa570d7bcc1b3e46ee127c4489fd25f4daa3",
+ "revisionTime": "2017-03-21T17:14:25Z"
+ },
+ {
+ "checksumSHA1": "j9kYvq02nJOTQmEH3wUw2Z/ybd8=",
+ "path": "github.com/docker/distribution/registry/auth",
+ "revision": "0700fa570d7bcc1b3e46ee127c4489fd25f4daa3",
+ "revisionTime": "2017-03-21T17:14:25Z"
+ },
+ {
+ "checksumSHA1": "09n22FzloMhoNbsxOT46PHGZaJA=",
+ "path": "github.com/docker/distribution/registry/auth/token",
+ "revision": "0700fa570d7bcc1b3e46ee127c4489fd25f4daa3",
+ "revisionTime": "2017-03-21T17:14:25Z"
+ },
+ {
+ "checksumSHA1": "tNWbcdclT5h9ygMIsq3VG05S6B0=",
+ "path": "github.com/docker/distribution/uuid",
+ "revision": "0700fa570d7bcc1b3e46ee127c4489fd25f4daa3",
+ "revisionTime": "2017-03-21T17:14:25Z"
+ },
+ {
+ "checksumSHA1": "SEVXNIcbfW8UK108uGqG1Lk81zo=",
+ "path": "github.com/docker/libtrust",
+ "revision": "aabc10ec26b754e797f9028f4589c5b7bd90dc20",
+ "revisionTime": "2016-07-08T17:25:13Z"
+ },
+ {
+ "checksumSHA1": "ej5H4I19BGtX42Dfe1ozH1Gcd6k=",
+ "path": "github.com/docker/libtrust/testutil",
+ "revision": "aabc10ec26b754e797f9028f4589c5b7bd90dc20",
+ "revisionTime": "2016-07-08T17:25:13Z"
+ },
+ {
+ "checksumSHA1": "tih88XkJ8c/nGHdMsRoi10N64fQ=",
+ "path": "github.com/facebookgo/clock",
+ "revision": "600d898af40aa09a7a93ecb9265d87b0504b6f03",
+ "revisionTime": "2015-04-10T01:09:13Z"
+ },
+ {
+ "checksumSHA1": "+jzvj7Vad92fiskWGxOPWPHL0w0=",
+ "path": "github.com/facebookgo/httpdown",
+ "revision": "a3b1354551a26449fbe05f5d855937f6e7acbd71",
+ "revisionTime": "2016-03-23T22:10:27Z"
+ },
+ {
+ "checksumSHA1": "BMOlXPELdp72k01fl9wUEKawQ+U=",
+ "path": "github.com/facebookgo/stats",
+ "revision": "1b76add642e42c6ffba7211ad7b3939ce654526e",
+ "revisionTime": "2015-10-06T22:16:25Z"
+ },
+ {
+ "checksumSHA1": "RR3M5cSlp1ltM7oToZif2QzZz/c=",
+ "path": "github.com/go-ldap/ldap",
+ "revision": "13cedcf58a1ea124045dea529a66c849d3444c8e",
+ "revisionTime": "2017-03-05T04:08:57Z"
+ },
+ {
+ "checksumSHA1": "yUc84k7cfnRi9AlPFuRo77Y18Og=",
+ "path": "github.com/golang/glog",
+ "revision": "23def4e6c14b4da8ac2ed8007337bc5eb5007998",
+ "revisionTime": "2016-01-25T20:49:56Z"
+ },
+ {
+ "checksumSHA1": "IhK3rKOSR3UfWHe5JmYv7Fnmkrk=",
+ "path": "github.com/golang/snappy",
+ "revision": "553a641470496b2327abcac10b36396bd98e45c9",
+ "revisionTime": "2017-02-15T23:32:05Z"
+ },
+ {
+ "checksumSHA1": "Bev6wUWj9ao5HvSK9NkV4EahnS0=",
+ "origin": "github.com/docker/distribution/vendor/github.com/gorilla/context",
+ "path": "github.com/gorilla/context",
+ "revision": "0700fa570d7bcc1b3e46ee127c4489fd25f4daa3",
+ "revisionTime": "2017-03-21T17:14:25Z"
+ },
+ {
+ "checksumSHA1": "mfR6uobHATtDFPT+GU2BLm68Kdo=",
+ "origin": "github.com/docker/distribution/vendor/github.com/gorilla/mux",
+ "path": "github.com/gorilla/mux",
+ "revision": "0700fa570d7bcc1b3e46ee127c4489fd25f4daa3",
+ "revisionTime": "2017-03-21T17:14:25Z"
+ },
+ {
+ "checksumSHA1": "LAqhkCywKqkTvAu+MbIRnpeFnZA=",
+ "path": "github.com/jteeuwen/go-bindata",
+ "revision": "a0ff2567cfb70903282db057e799fd826784d41d",
+ "revisionTime": "2015-10-23T09:11:02Z"
+ },
+ {
+ "checksumSHA1": "1E4XSPmZqO/s20/WWJKXsyGTfm4=",
+ "path": "github.com/jteeuwen/go-bindata/go-bindata",
+ "revision": "a0ff2567cfb70903282db057e799fd826784d41d",
+ "revisionTime": "2015-10-23T09:11:02Z"
+ },
+ {
+ "checksumSHA1": "qafPKAxcGTNAL1S6Ly6hs4hVT8k=",
+ "path": "github.com/onsi/ginkgo",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "R0SNYpmISoxRcVWhRXLmWbOkqzY=",
+ "path": "github.com/onsi/ginkgo/config",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "Djfgj1lp/1lLdZvC6dfpe1xFWAU=",
+ "path": "github.com/onsi/ginkgo/internal/codelocation",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "bk3fPLYoCcaMCQCkr7l2F1k9qvE=",
+ "path": "github.com/onsi/ginkgo/internal/containernode",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "YXukUqtSZk/6dOoq02fRfp7HBtI=",
+ "path": "github.com/onsi/ginkgo/internal/failer",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "syt+qozqwaniN4cID+xdRyElZPc=",
+ "path": "github.com/onsi/ginkgo/internal/leafnodes",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "khPjshQRAjjziEG3ZY6Kwj1q0WA=",
+ "path": "github.com/onsi/ginkgo/internal/remote",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "biCzwu1LHcTDe9mVhb/CBvgKEl8=",
+ "path": "github.com/onsi/ginkgo/internal/spec",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "mbabEQNCg0JrWvAO/Sws2lICo3E=",
+ "path": "github.com/onsi/ginkgo/internal/specrunner",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "38Z+VxVz5LlFqoYGj2TO4u+BeFY=",
+ "path": "github.com/onsi/ginkgo/internal/suite",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "6sOxGhw1brknfjJd9h7MhpnxHMY=",
+ "path": "github.com/onsi/ginkgo/internal/testingtproxy",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "eGp6kNfhzh4z8EtiakksnO0+HWI=",
+ "path": "github.com/onsi/ginkgo/internal/writer",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "DrVQM/1iTuIyvayBTnuB6pWS51M=",
+ "path": "github.com/onsi/ginkgo/reporters",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "LFlv3sbzEoTcjS6gtctz/HLO74g=",
+ "path": "github.com/onsi/ginkgo/reporters/stenographer",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "a/OSgDX/WOpy+u4Lm1FVK57+w2k=",
+ "path": "github.com/onsi/ginkgo/types",
+ "revision": "25380c62e61d3f90436be125b8127dbae578fdef",
+ "revisionTime": "2015-06-27T18:45:31Z"
+ },
+ {
+ "checksumSHA1": "F2nzbTb5VserStm8/ZYLt7m6EMk=",
+ "path": "github.com/onsi/gomega",
+ "revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
+ "revisionTime": "2015-05-30T21:13:11Z"
+ },
+ {
+ "checksumSHA1": "8v+lUNp0/5+3QZtUrJ7+DKFTL2U=",
+ "path": "github.com/onsi/gomega/format",
+ "revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
+ "revisionTime": "2015-05-30T21:13:11Z"
+ },
+ {
+ "checksumSHA1": "ZzBlmFKcNKJ/rSy0Quv6d/gCZ3w=",
+ "path": "github.com/onsi/gomega/gbytes",
+ "revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
+ "revisionTime": "2015-05-30T21:13:11Z"
+ },
+ {
+ "checksumSHA1": "H3kuVM+3XZZciTLxPOLh9YzhD+U=",
+ "path": "github.com/onsi/gomega/ghttp",
+ "revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
+ "revisionTime": "2015-05-30T21:13:11Z"
+ },
+ {
+ "checksumSHA1": "wU1avNsF7pL+O8CV39/FoOUJZlY=",
+ "path": "github.com/onsi/gomega/internal/assertion",
+ "revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
+ "revisionTime": "2015-05-30T21:13:11Z"
+ },
+ {
+ "checksumSHA1": "Yt5Wlj9S7Tu/ZClZOiOa5ombnQY=",
+ "path": "github.com/onsi/gomega/internal/asyncassertion",
+ "revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
+ "revisionTime": "2015-05-30T21:13:11Z"
+ },
+ {
+ "checksumSHA1": "CIwteJCVKKQDPoXfqtVTVMHAeEU=",
+ "path": "github.com/onsi/gomega/internal/fakematcher",
+ "revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
+ "revisionTime": "2015-05-30T21:13:11Z"
+ },
+ {
+ "checksumSHA1": "ufjDjg83v7dVAlpc5qjkH6ATInU=",
+ "path": "github.com/onsi/gomega/internal/testingtsupport",
+ "revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
+ "revisionTime": "2015-05-30T21:13:11Z"
+ },
+ {
+ "checksumSHA1": "xOIZnI9Qj0TQhrMEI5QrosxK6ls=",
+ "path": "github.com/onsi/gomega/matchers",
+ "revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
+ "revisionTime": "2015-05-30T21:13:11Z"
+ },
+ {
+ "checksumSHA1": "gWJfsDHiZTga1esEChd+MfpftN0=",
+ "path": "github.com/onsi/gomega/matchers/support/goraph/bipartitegraph",
+ "revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
+ "revisionTime": "2015-05-30T21:13:11Z"
+ },
+ {
+ "checksumSHA1": "zjTC6ady0bJUwzTFAKtv63T7Fmg=",
+ "path": "github.com/onsi/gomega/matchers/support/goraph/edge",
+ "revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
+ "revisionTime": "2015-05-30T21:13:11Z"
+ },
+ {
+ "checksumSHA1": "o2+IscLOPKOiovl2g0/igkD1R4Q=",
+ "path": "github.com/onsi/gomega/matchers/support/goraph/node",
+ "revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
+ "revisionTime": "2015-05-30T21:13:11Z"
+ },
+ {
+ "checksumSHA1": "W1zfga0jmo7Daetjcur8v2hh0y8=",
+ "path": "github.com/onsi/gomega/matchers/support/goraph/util",
+ "revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
+ "revisionTime": "2015-05-30T21:13:11Z"
+ },
+ {
+ "checksumSHA1": "zEuQ0PUlbqojtNwvu01Mn3DQCJM=",
+ "path": "github.com/onsi/gomega/types",
+ "revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
+ "revisionTime": "2015-05-30T21:13:11Z"
+ },
+ {
+ "checksumSHA1": "GVY3lzvj4xmpKOGgA4/h9GWjQVk=",
+ "path": "github.com/syndtr/goleveldb/leveldb",
+ "revision": "3c5717caf1475fd25964109a0fc640bd150fce43",
+ "revisionTime": "2017-03-02T03:19:10Z"
+ },
+ {
+ "checksumSHA1": "qITi3AleZHNDuMD7tEeP2NMOEwk=",
+ "path": "github.com/syndtr/goleveldb/leveldb/cache",
+ "revision": "3c5717caf1475fd25964109a0fc640bd150fce43",
+ "revisionTime": "2017-03-02T03:19:10Z"
+ },
+ {
+ "checksumSHA1": "5KPgnvCPlR0ysDAqo6jApzRQ3tw=",
+ "path": "github.com/syndtr/goleveldb/leveldb/comparer",
+ "revision": "3c5717caf1475fd25964109a0fc640bd150fce43",
+ "revisionTime": "2017-03-02T03:19:10Z"
+ },
+ {
+ "checksumSHA1": "1DRAxdlWzS4U0xKN/yQ/fdNN7f0=",
+ "path": "github.com/syndtr/goleveldb/leveldb/errors",
+ "revision": "3c5717caf1475fd25964109a0fc640bd150fce43",
+ "revisionTime": "2017-03-02T03:19:10Z"
+ },
+ {
+ "checksumSHA1": "8ssfBXjxEDGEmP9zZy+131Zpfig=",
+ "path": "github.com/syndtr/goleveldb/leveldb/filter",
+ "revision": "3c5717caf1475fd25964109a0fc640bd150fce43",
+ "revisionTime": "2017-03-02T03:19:10Z"
+ },
+ {
+ "checksumSHA1": "P5o3zyHp2reDi4OSnNGbXQcoT7s=",
+ "path": "github.com/syndtr/goleveldb/leveldb/iterator",
+ "revision": "3c5717caf1475fd25964109a0fc640bd150fce43",
+ "revisionTime": "2017-03-02T03:19:10Z"
+ },
+ {
+ "checksumSHA1": "3uB/sw+/s5EAXI/CTDx29Fed0kw=",
+ "path": "github.com/syndtr/goleveldb/leveldb/journal",
+ "revision": "3c5717caf1475fd25964109a0fc640bd150fce43",
+ "revisionTime": "2017-03-02T03:19:10Z"
+ },
+ {
+ "checksumSHA1": "kCsyA+AaHsJ+TEyefgxRjSW+w/0=",
+ "path": "github.com/syndtr/goleveldb/leveldb/memdb",
+ "revision": "3c5717caf1475fd25964109a0fc640bd150fce43",
+ "revisionTime": "2017-03-02T03:19:10Z"
+ },
+ {
+ "checksumSHA1": "UmQeotV+m8/FduKEfLOhjdp18rs=",
+ "path": "github.com/syndtr/goleveldb/leveldb/opt",
+ "revision": "3c5717caf1475fd25964109a0fc640bd150fce43",
+ "revisionTime": "2017-03-02T03:19:10Z"
+ },
+ {
+ "checksumSHA1": "XO6uR3Wew1G4uyHN8I9Gvl7twKY=",
+ "path": "github.com/syndtr/goleveldb/leveldb/storage",
+ "revision": "3c5717caf1475fd25964109a0fc640bd150fce43",
+ "revisionTime": "2017-03-02T03:19:10Z"
+ },
+ {
+ "checksumSHA1": "ne6LKzh6wCQQ05mnQ6kDyGHRyvs=",
+ "path": "github.com/syndtr/goleveldb/leveldb/table",
+ "revision": "3c5717caf1475fd25964109a0fc640bd150fce43",
+ "revisionTime": "2017-03-02T03:19:10Z"
+ },
+ {
+ "checksumSHA1": "eyYwOkjZtIdWyde94zaTS4RS22M=",
+ "path": "github.com/syndtr/goleveldb/leveldb/testutil",
+ "revision": "3c5717caf1475fd25964109a0fc640bd150fce43",
+ "revisionTime": "2017-03-02T03:19:10Z"
+ },
+ {
+ "checksumSHA1": "LZb6+6ryx3zxm5VxgQcIJyfdu6E=",
+ "path": "github.com/syndtr/goleveldb/leveldb/util",
+ "revision": "3c5717caf1475fd25964109a0fc640bd150fce43",
+ "revisionTime": "2017-03-02T03:19:10Z"
+ },
+ {
+ "checksumSHA1": "5HymxKSV8zcw6eiNV5Z/MZBPmuU=",
+ "path": "golang.org/x/crypto/bcrypt",
+ "revision": "573951cbe80bb6352881271bb276f48749eab6f4",
+ "revisionTime": "2017-03-30T09:09:28Z"
+ },
+ {
+ "checksumSHA1": "nkSU+D5ZACy+jm3aEB+Sr5t0Eqc=",
+ "path": "golang.org/x/crypto/blowfish",
+ "revision": "573951cbe80bb6352881271bb276f48749eab6f4",
+ "revisionTime": "2017-03-30T09:09:28Z"
+ },
+ {
+ "checksumSHA1": "9QKY4bQlbQ/VZZFP8/1WtH4b0mA=",
+ "origin": "github.com/docker/distribution/vendor/golang.org/x/net/context",
+ "path": "golang.org/x/net/context",
+ "revision": "0700fa570d7bcc1b3e46ee127c4489fd25f4daa3",
+ "revisionTime": "2017-03-21T17:14:25Z"
+ },
+ {
+ "checksumSHA1": "AK65RmsGNBl0/e11OVrf2mW78gU=",
+ "path": "golang.org/x/sys/unix",
+ "revision": "493114f68206f85e7e333beccfabc11e98cba8dd",
+ "revisionTime": "2017-03-31T21:25:38Z"
+ },
+ {
+ "checksumSHA1": "fRERF7JFq7KYgM9I48onMlEgFm4=",
+ "path": "gopkg.in/asn1-ber.v1",
+ "revision": "4e86f4367175e39f69d9358a5f17b4dda270378d",
+ "revisionTime": "2015-09-24T05:17:56Z"
+ },
+ {
+ "checksumSHA1": "nGXjr6oY5leYbIOJNkJmHGiBp38=",
+ "path": "gopkg.in/fsnotify.v1",
+ "revision": "629574ca2a5df945712d3079857300b5e4da0236",
+ "revisionTime": "2016-10-11T02:33:12Z"
+ },
+ {
+ "checksumSHA1": "tefd2MHMp6qRXeE3jqzp3P40mNI=",
+ "path": "gopkg.in/mgo.v2",
+ "revision": "3f83fa5005286a7fe593b055f0d7771a7dce4655",
+ "revisionTime": "2016-08-18T02:01:20Z"
+ },
+ {
+ "checksumSHA1": "+mZKlPX9t4fHOQvkQeCnWw5JjkQ=",
+ "path": "gopkg.in/mgo.v2/bson",
+ "revision": "3f83fa5005286a7fe593b055f0d7771a7dce4655",
+ "revisionTime": "2016-08-18T02:01:20Z"
+ },
+ {
+ "checksumSHA1": "trGhCcEZSOZTwlLfWCRAoXXihW8=",
+ "path": "gopkg.in/mgo.v2/internal/json",
+ "revision": "3f83fa5005286a7fe593b055f0d7771a7dce4655",
+ "revisionTime": "2016-08-18T02:01:20Z"
+ },
+ {
+ "checksumSHA1": "LEvMCnprte47qdAxWvQ/zRxVF1U=",
+ "path": "gopkg.in/mgo.v2/internal/sasl",
+ "revision": "3f83fa5005286a7fe593b055f0d7771a7dce4655",
+ "revisionTime": "2016-08-18T02:01:20Z"
+ },
+ {
+ "checksumSHA1": "fOxeCpKRYNTTM2VCrsU2S/KEd1s=",
+ "path": "gopkg.in/mgo.v2/internal/scram",
+ "revision": "3f83fa5005286a7fe593b055f0d7771a7dce4655",
+ "revisionTime": "2016-08-18T02:01:20Z"
+ },
+ {
+ "checksumSHA1": "/0kOHaD3bhhN1GjmZajSigSqu4E=",
+ "path": "gopkg.in/yaml.v2",
+ "revision": "a3f3340b5840cee44f372bddb5880fcbc419b46a",
+ "revisionTime": "2017-02-08T14:18:51Z"
+ }
+ ],
+ "rootPath": "github.com/cesanta/docker_auth/auth_server"
+}
From 4f7154faefefc7010e33ad7a18f14489e9fab2ac Mon Sep 17 00:00:00 2001
From: Roman Vynar
Date: Wed, 5 Apr 2017 17:27:04 +0300
Subject: [PATCH 062/209] Fix checking of service field in ACL. (#170)
---
auth_server/authz/acl.go | 6 ++++--
auth_server/authz/acl_test.go | 11 +++++++++--
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/auth_server/authz/acl.go b/auth_server/authz/acl.go
index e58e7fd2..96e55b78 100644
--- a/auth_server/authz/acl.go
+++ b/auth_server/authz/acl.go
@@ -27,6 +27,7 @@ type MatchConditions struct {
Type *string `yaml:"type,omitempty" json:"type,omitempty"`
Name *string `yaml:"name,omitempty" json:"name,omitempty"`
IP *string `yaml:"ip,omitempty" json:"ip,omitempty"`
+ Service *string `yaml:"service,omitempty" json:"service,omitempty"`
Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
}
@@ -64,7 +65,7 @@ func parseIPPattern(ipp string) (*net.IPNet, error) {
}
func validateMatchConditions(mc *MatchConditions) error {
- for _, p := range []*string{mc.Account, mc.Type, mc.Name} {
+ for _, p := range []*string{mc.Account, mc.Type, mc.Name, mc.Service} {
if p == nil {
continue
}
@@ -201,7 +202,7 @@ func (mc *MatchConditions) Matches(ai *AuthRequestInfo) bool {
"${name}", regexp.QuoteMeta(ai.Name),
"${service}", regexp.QuoteMeta(ai.Service),
}
- for _, x := range []string{"Account", "Type", "Name"} {
+ for _, x := range []string{"Account", "Type", "Name", "Service"} {
field, _ := getField(mc, x)
for _, found := range captureGroupRegex.FindAllStringSubmatch(field, -1) {
key := strings.Title(found[1])
@@ -235,6 +236,7 @@ func (mc *MatchConditions) Matches(ai *AuthRequestInfo) bool {
return matchString(mc.Account, ai.Account, vars) &&
matchString(mc.Type, ai.Type, vars) &&
matchString(mc.Name, ai.Name, vars) &&
+ matchString(mc.Service, ai.Service, vars) &&
matchIP(mc.IP, ai.IP) &&
matchLabels(mc.Labels, ai.Labels, vars)
}
diff --git a/auth_server/authz/acl_test.go b/auth_server/authz/acl_test.go
index 51bb484d..4e6b2477 100644
--- a/auth_server/authz/acl_test.go
+++ b/auth_server/authz/acl_test.go
@@ -25,6 +25,9 @@ func TestValidation(t *testing.T) {
{MatchConditions{Name: sp("foo")}, true},
{MatchConditions{Name: sp("foo?*")}, true},
{MatchConditions{Name: sp("/foo.*/")}, true},
+ {MatchConditions{Service: sp("foo")}, true},
+ {MatchConditions{Service: sp("foo?*")}, true},
+ {MatchConditions{Service: sp("/foo.*/")}, true},
{MatchConditions{IP: sp("192.168.0.1")}, true},
{MatchConditions{IP: sp("192.168.0.0/16")}, true},
{MatchConditions{IP: sp("2001:db8::1")}, true},
@@ -34,6 +37,7 @@ func TestValidation(t *testing.T) {
{MatchConditions{Account: sp("/foo?*/")}, false},
{MatchConditions{Type: sp("/foo?*/")}, false},
{MatchConditions{Name: sp("/foo?*/")}, false},
+ {MatchConditions{Service: sp("/foo?*/")}, false},
{MatchConditions{IP: sp("192.168.0.1/100")}, false},
{MatchConditions{IP: sp("192.168.0.*")}, false},
{MatchConditions{IP: sp("foo")}, false},
@@ -51,8 +55,8 @@ func TestValidation(t *testing.T) {
}
func TestMatching(t *testing.T) {
- ai1 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz"}
- ai2 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz",
+ ai1 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz", Service: "notary"}
+ ai2 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz", Service: "notary",
Labels: map[string][]string{"group": []string{"admins", "VIP"}}}
cases := []struct {
mc MatchConditions
@@ -71,6 +75,9 @@ func TestMatching(t *testing.T) {
{MatchConditions{Account: sp(`/^(.+)@test\.com$/`), Name: sp(`${account:1}/*`)}, AuthRequestInfo{Account: "john.smith@test.com", Name: "john.smith/test"}, true},
{MatchConditions{Account: sp(`/^(.+)@test\.com$/`), Name: sp(`${account:3}/*`)}, AuthRequestInfo{Account: "john.smith@test.com", Name: "john.smith/test"}, false},
{MatchConditions{Account: sp(`/^(.+)@(.+?).test\.com$/`), Name: sp(`${account:1}-${account:2}/*`)}, AuthRequestInfo{Account: "john.smith@it.test.com", Name: "john.smith-it/test"}, true},
+ {MatchConditions{Service: sp("notary"), Type: sp("bar")}, ai1, true},
+ {MatchConditions{Service: sp("notary"), Type: sp("baz")}, ai1, false},
+ {MatchConditions{Service: sp("notary1"), Type: sp("bar")}, ai1, false},
// IP matching
{MatchConditions{IP: sp("127.0.0.1")}, AuthRequestInfo{IP: nil}, false},
{MatchConditions{IP: sp("127.0.0.1")}, AuthRequestInfo{IP: net.IPv4(127, 0, 0, 1)}, true},
From a53bceffb610b3a7f1ab10f332bf48048e75a8d4 Mon Sep 17 00:00:00 2001
From: rojer
Date: Wed, 19 Apr 2017 13:57:29 +0100
Subject: [PATCH 063/209] Fix auth page content type
Fixes #172
---
auth_server/server/server.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index be5a2947..78919862 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -349,7 +349,7 @@ func (as *AuthServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// https://developers.google.com/identity/sign-in/web/server-side-flow
func (as *AuthServer) doIndex(rw http.ResponseWriter, req *http.Request) {
- rw.Header().Set("Content-Type", "text-html; charset=utf-8")
+ rw.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(rw, "%s
\n", as.config.Token.Issuer)
if as.ga != nil {
fmt.Fprint(rw, `Login with Google account
`)
From 721c841bb9297e9ae0025ce5a77486380d7125c4 Mon Sep 17 00:00:00 2001
From: rojer
Date: Wed, 19 Apr 2017 17:38:26 +0100
Subject: [PATCH 064/209] Build a static binary
Fixes #173
---
auth_server/Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/auth_server/Makefile b/auth_server/Makefile
index 25369812..120100dc 100644
--- a/auth_server/Makefile
+++ b/auth_server/Makefile
@@ -17,7 +17,7 @@ deps:
build:
go generate ./...
- go build -v -i
+ CGO_ENABLED=0 go build -v -i --ldflags=--s
ca-certificates.crt:
cp $(CA_BUNDLE) .
From d76a69c31cdef1ea1c21b0c675aaeaef6d87594f Mon Sep 17 00:00:00 2001
From: rojer
Date: Wed, 19 Apr 2017 19:43:40 +0100
Subject: [PATCH 065/209] Use cesanta/glog instead of golang/glog
For --logbufsecs
---
auth_server/authn/ext_auth.go | 2 +-
auth_server/authn/github_auth.go | 2 +-
auth_server/authn/google_auth.go | 2 +-
auth_server/authn/ldap_auth.go | 2 +-
auth_server/authn/mongo_auth.go | 2 +-
auth_server/authn/tokendb.go | 2 +-
auth_server/authz/acl.go | 2 +-
auth_server/authz/acl_mongo.go | 2 +-
auth_server/authz/ext_authz.go | 2 +-
auth_server/main.go | 2 +-
auth_server/mgo_session/mgo_session.go | 2 +-
auth_server/server/server.go | 2 +-
auth_server/vendor/vendor.json | 12 ++++++------
13 files changed, 18 insertions(+), 18 deletions(-)
diff --git a/auth_server/authn/ext_auth.go b/auth_server/authn/ext_auth.go
index 9a1709ef..c26b660e 100644
--- a/auth_server/authn/ext_auth.go
+++ b/auth_server/authn/ext_auth.go
@@ -23,7 +23,7 @@ import (
"strings"
"syscall"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
)
type ExtAuthConfig struct {
diff --git a/auth_server/authn/github_auth.go b/auth_server/authn/github_auth.go
index 7e1a9445..0ce9c5ce 100644
--- a/auth_server/authn/github_auth.go
+++ b/auth_server/authn/github_auth.go
@@ -28,7 +28,7 @@ import (
"strings"
"time"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
)
type GitHubAuthConfig struct {
diff --git a/auth_server/authn/google_auth.go b/auth_server/authn/google_auth.go
index 93f1a94a..10891ab8 100644
--- a/auth_server/authn/google_auth.go
+++ b/auth_server/authn/google_auth.go
@@ -27,7 +27,7 @@ import (
"strings"
"time"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
)
type GoogleAuthConfig struct {
diff --git a/auth_server/authn/ldap_auth.go b/auth_server/authn/ldap_auth.go
index f8fc08ff..3bdf7c39 100644
--- a/auth_server/authn/ldap_auth.go
+++ b/auth_server/authn/ldap_auth.go
@@ -24,7 +24,7 @@ import (
"strings"
"github.com/go-ldap/ldap"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
)
type LDAPAuthConfig struct {
diff --git a/auth_server/authn/mongo_auth.go b/auth_server/authn/mongo_auth.go
index 165fee40..72d08689 100644
--- a/auth_server/authn/mongo_auth.go
+++ b/auth_server/authn/mongo_auth.go
@@ -23,7 +23,7 @@ import (
"time"
"github.com/cesanta/docker_auth/auth_server/mgo_session"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
"golang.org/x/crypto/bcrypt"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
diff --git a/auth_server/authn/tokendb.go b/auth_server/authn/tokendb.go
index daaec171..a7d8bf0a 100644
--- a/auth_server/authn/tokendb.go
+++ b/auth_server/authn/tokendb.go
@@ -25,7 +25,7 @@ import (
"golang.org/x/crypto/bcrypt"
"github.com/dchest/uniuri"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
"github.com/syndtr/goleveldb/leveldb"
)
diff --git a/auth_server/authz/acl.go b/auth_server/authz/acl.go
index 96e55b78..19d36a91 100644
--- a/auth_server/authz/acl.go
+++ b/auth_server/authz/acl.go
@@ -11,7 +11,7 @@ import (
"strings"
"github.com/cesanta/docker_auth/auth_server/authn"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
)
type ACL []ACLEntry
diff --git a/auth_server/authz/acl_mongo.go b/auth_server/authz/acl_mongo.go
index b9194662..a5441103 100644
--- a/auth_server/authz/acl_mongo.go
+++ b/auth_server/authz/acl_mongo.go
@@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"github.com/cesanta/docker_auth/auth_server/mgo_session"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"io"
diff --git a/auth_server/authz/ext_authz.go b/auth_server/authz/ext_authz.go
index fdf00316..98890214 100644
--- a/auth_server/authz/ext_authz.go
+++ b/auth_server/authz/ext_authz.go
@@ -23,7 +23,7 @@ import (
"strings"
"syscall"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
)
type ExtAuthzConfig struct {
diff --git a/auth_server/main.go b/auth_server/main.go
index ad70282f..43b0d169 100644
--- a/auth_server/main.go
+++ b/auth_server/main.go
@@ -30,7 +30,7 @@ import (
"github.com/cesanta/docker_auth/auth_server/server"
"github.com/facebookgo/httpdown"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
fsnotify "gopkg.in/fsnotify.v1"
)
diff --git a/auth_server/mgo_session/mgo_session.go b/auth_server/mgo_session/mgo_session.go
index 5d1d2ba2..2f3dd93e 100644
--- a/auth_server/mgo_session/mgo_session.go
+++ b/auth_server/mgo_session/mgo_session.go
@@ -22,7 +22,7 @@ import (
"strings"
"time"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
"gopkg.in/mgo.v2"
)
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 78919862..5d4f6571 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -31,7 +31,7 @@ import (
"github.com/cesanta/docker_auth/auth_server/authn"
"github.com/cesanta/docker_auth/auth_server/authz"
"github.com/docker/distribution/registry/auth/token"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
)
var (
diff --git a/auth_server/vendor/vendor.json b/auth_server/vendor/vendor.json
index e0f1ec28..50c9b8ae 100644
--- a/auth_server/vendor/vendor.json
+++ b/auth_server/vendor/vendor.json
@@ -9,6 +9,12 @@
"revision": "0700fa570d7bcc1b3e46ee127c4489fd25f4daa3",
"revisionTime": "2017-03-21T17:14:25Z"
},
+ {
+ "checksumSHA1": "60hRVfzu19loKucKk/vvBkuRfpg=",
+ "path": "github.com/cesanta/glog",
+ "revision": "22eb27a0ae192b290b25537b8e876556fc25129c",
+ "revisionTime": "2015-05-27T11:16:57Z"
+ },
{
"checksumSHA1": "drfK6NPoGhSk7ZD6uhL+JjEpKy4=",
"path": "github.com/dchest/uniuri",
@@ -81,12 +87,6 @@
"revision": "13cedcf58a1ea124045dea529a66c849d3444c8e",
"revisionTime": "2017-03-05T04:08:57Z"
},
- {
- "checksumSHA1": "yUc84k7cfnRi9AlPFuRo77Y18Og=",
- "path": "github.com/golang/glog",
- "revision": "23def4e6c14b4da8ac2ed8007337bc5eb5007998",
- "revisionTime": "2016-01-25T20:49:56Z"
- },
{
"checksumSHA1": "IhK3rKOSR3UfWHe5JmYv7Fnmkrk=",
"path": "github.com/golang/snappy",
From a76426387685c721962e84e29f60c85456b28e86 Mon Sep 17 00:00:00 2001
From: rojer
Date: Fri, 2 Jun 2017 14:32:37 +0100
Subject: [PATCH 066/209] Add support for LetsEncrypt
---
auth_server/Makefile | 2 +-
auth_server/main.go | 46 +++++++++++++++++++++-------------
auth_server/server/config.go | 31 +++++++++++++++++++----
auth_server/server/server.go | 2 +-
auth_server/vendor/vendor.json | 6 +++++
examples/reference.yml | 19 ++++++++++++--
6 files changed, 79 insertions(+), 27 deletions(-)
diff --git a/auth_server/Makefile b/auth_server/Makefile
index 120100dc..9812918e 100644
--- a/auth_server/Makefile
+++ b/auth_server/Makefile
@@ -40,7 +40,7 @@ auth_server:
@echo
@exit 1
-docker-build: auth_server
+docker-build: build
docker build -t $(IMAGE):latest .
docker tag $(IMAGE):latest $(IMAGE):$(VERSION)
diff --git a/auth_server/main.go b/auth_server/main.go
index 43b0d169..9694917d 100644
--- a/auth_server/main.go
+++ b/auth_server/main.go
@@ -29,8 +29,9 @@ import (
"time"
"github.com/cesanta/docker_auth/auth_server/server"
- "github.com/facebookgo/httpdown"
"github.com/cesanta/glog"
+ "github.com/facebookgo/httpdown"
+ "golang.org/x/crypto/acme/autocert"
fsnotify "gopkg.in/fsnotify.v1"
)
@@ -48,34 +49,43 @@ func ServeOnce(c *server.Config, cf string, hd *httpdown.HTTP) (*server.AuthServ
glog.Exitf("Failed to create auth server: %s", err)
}
- var tlsConfig *tls.Config
+ tlsConfig := &tls.Config{
+ MinVersion: tls.VersionTLS10,
+ PreferServerCipherSuites: true,
+ CipherSuites: []uint16{
+ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+ tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+ tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+ tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+ tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+ tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+ tls.TLS_RSA_WITH_AES_128_CBC_SHA,
+ tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+ },
+ NextProtos: []string{"http/1.1"},
+ }
if c.Server.CertFile != "" || c.Server.KeyFile != "" {
// Check for partial configuration.
if c.Server.CertFile == "" || c.Server.KeyFile == "" {
glog.Exitf("Failed to load certificate and key: both were not provided")
}
- tlsConfig = &tls.Config{
- MinVersion: tls.VersionTLS10,
- PreferServerCipherSuites: true,
- CipherSuites: []uint16{
- tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
- tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
- tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
- tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
- tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
- tls.TLS_RSA_WITH_AES_128_CBC_SHA,
- tls.TLS_RSA_WITH_AES_256_CBC_SHA,
- },
- NextProtos: []string{"http/1.1"},
- Certificates: make([]tls.Certificate, 1),
- }
glog.Infof("Cert file: %s", c.Server.CertFile)
glog.Infof("Key file : %s", c.Server.KeyFile)
+ tlsConfig.Certificates = make([]tls.Certificate, 1)
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(c.Server.CertFile, c.Server.KeyFile)
if err != nil {
glog.Exitf("Failed to load certificate and key: %s", err)
}
+ } else if c.Server.LetsEncrypt.Email != "" {
+ m := &autocert.Manager{
+ Email: c.Server.LetsEncrypt.Email,
+ Prompt: autocert.AcceptTOS,
+ }
+ if c.Server.LetsEncrypt.Host != "" {
+ m.HostPolicy = autocert.HostWhitelist(c.Server.LetsEncrypt.Host)
+ }
+ glog.Infof("Using LetsEncrypt, host %q, email %q", c.Server.LetsEncrypt.Host, c.Server.LetsEncrypt.Email)
+ tlsConfig.GetCertificate = m.GetCertificate
} else {
glog.Warning("Running without TLS")
}
diff --git a/auth_server/server/config.go b/auth_server/server/config.go
index f6e53092..391fa9f7 100644
--- a/auth_server/server/config.go
+++ b/auth_server/server/config.go
@@ -22,6 +22,7 @@ import (
"errors"
"fmt"
"io/ioutil"
+ "os"
"strings"
"time"
@@ -46,16 +47,23 @@ type Config struct {
}
type ServerConfig struct {
- ListenAddress string `yaml:"addr,omitempty"`
- RealIPHeader string `yaml:"real_ip_header,omitempty"`
- RealIPPos int `yaml:"real_ip_pos,omitempty"`
- CertFile string `yaml:"certificate,omitempty"`
- KeyFile string `yaml:"key,omitempty"`
+ ListenAddress string `yaml:"addr,omitempty"`
+ RealIPHeader string `yaml:"real_ip_header,omitempty"`
+ RealIPPos int `yaml:"real_ip_pos,omitempty"`
+ CertFile string `yaml:"certificate,omitempty"`
+ KeyFile string `yaml:"key,omitempty"`
+ LetsEncrypt LetsEncryptConfig `yaml:"letsencrypt,omitempty"`
publicKey libtrust.PublicKey
privateKey libtrust.PrivateKey
}
+type LetsEncryptConfig struct {
+ Host string `yaml:"host,omitempty"`
+ Email string `yaml:"email,omitempty"`
+ CacheDir string `yaml:"cache_dir,omitempty"`
+}
+
type TokenConfig struct {
Issuer string `yaml:"issuer,omitempty"`
CertFile string `yaml:"certificate,omitempty"`
@@ -208,5 +216,18 @@ func LoadConfig(fileName string) (*Config, error) {
if !tokenConfigured {
return nil, fmt.Errorf("failed to load token cert and key: none provided")
}
+
+ if !serverConfigured && c.Server.LetsEncrypt.Email != "" {
+ if c.Server.LetsEncrypt.CacheDir == "" {
+ return nil, fmt.Errorf("server.letsencrypt.cache_dir is required")
+ }
+ // We require that LetsEncrypt is an existing directory, because we really don't want it
+ // to be misconfigured and obtained certificates to be lost.
+ fi, err := os.Stat(c.Server.LetsEncrypt.CacheDir)
+ if err != nil || !fi.IsDir() {
+ return nil, fmt.Errorf("server.letsencrypt.cache_dir (%s) does not exist or is not a directory", c.Server.LetsEncrypt.CacheDir)
+ }
+ }
+
return c, nil
}
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 5d4f6571..5f0fa24c 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -30,8 +30,8 @@ import (
"github.com/cesanta/docker_auth/auth_server/authn"
"github.com/cesanta/docker_auth/auth_server/authz"
- "github.com/docker/distribution/registry/auth/token"
"github.com/cesanta/glog"
+ "github.com/docker/distribution/registry/auth/token"
)
var (
diff --git a/auth_server/vendor/vendor.json b/auth_server/vendor/vendor.json
index 50c9b8ae..f261b9f2 100644
--- a/auth_server/vendor/vendor.json
+++ b/auth_server/vendor/vendor.json
@@ -371,6 +371,12 @@
"revision": "3c5717caf1475fd25964109a0fc640bd150fce43",
"revisionTime": "2017-03-02T03:19:10Z"
},
+ {
+ "checksumSHA1": "EUHyLhfP6B9rbXf8avkDXn5FTqE=",
+ "path": "golang.org/x/crypto/acme",
+ "revision": "e1a4589e7d3ea14a3352255d04b6f1a418845e5e",
+ "revisionTime": "2017-05-23T23:42:09Z"
+ },
{
"checksumSHA1": "5HymxKSV8zcw6eiNV5Z/MZBPmuU=",
"path": "golang.org/x/crypto/bcrypt",
diff --git a/examples/reference.yml b/examples/reference.yml
index 716a8d01..2cf4957a 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -12,10 +12,25 @@
server: # Server settings.
# Address to listen on.
addr: ":5001"
- # TLS certificate and key.
- # If not specified, server will open a plain HTTP listener. In that case token.certificate and key must be provided.
+
+ # TLS options.
+ #
+ # Use specific certificate and key.
certificate: "/path/to/server.pem"
key: "/path/to/server.key"
+ # Use LetsEncrypt (https://letsencrypt.org/) to automatically obtain and maintain a certificate.
+ # Note that this only applies to server TLS certificate, this certificate will not be used for tokens
+ letsencrypt:
+ # Email is required. It will be used to register with LetsEncrypt.
+ email: webmaster@example.org
+ # Cache directory, where certificates issued by LE will be stored. Must exist.
+ # It is recommended to make it a volume mount so it persists across restarts.
+ cache_dir: /data/sslcache
+ # Normally LetsEncrypt will obtain a certificate for whichever host the client is connecting to.
+ # With this option, you can limit it to a specific host name.
+ # host: "docker.example.org"
+ # If neither certificate+key or letsencrypt are configured, the listener does not use TLS.
+
# Take client's address from the specified HTTP header instead of connection.
# May be useful if the server is behind a proxy or load balancer.
# If configured, this header must be present, requests without it will be rejected.
From d2bca957d854f9609682c8ebcea7a500e944ba01 Mon Sep 17 00:00:00 2001
From: rojer
Date: Tue, 6 Jun 2017 23:40:05 +0100
Subject: [PATCH 067/209] Fix non-TLS mode
Fixes #180
---
auth_server/main.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/auth_server/main.go b/auth_server/main.go
index 9694917d..2337e7ba 100644
--- a/auth_server/main.go
+++ b/auth_server/main.go
@@ -88,6 +88,7 @@ func ServeOnce(c *server.Config, cf string, hd *httpdown.HTTP) (*server.AuthServ
tlsConfig.GetCertificate = m.GetCertificate
} else {
glog.Warning("Running without TLS")
+ tlsConfig = nil
}
hs := &http.Server{
Addr: c.Server.ListenAddress,
From 6a3bc8c52bcd5f12cf870dbc3b9e2e138ceae9bc Mon Sep 17 00:00:00 2001
From: Alon Bar-Lev
Date: Tue, 11 Jul 2017 10:05:33 +0300
Subject: [PATCH 068/209] Feature: URL Prefix support (#181) (#185)
Usable when service is behind a proxy.
Tested with docker-distribution and apache httpd.
---
auth_server/server/config.go | 4 ++++
auth_server/server/server.go | 9 +++++----
examples/reference.yml | 3 +++
3 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/auth_server/server/config.go b/auth_server/server/config.go
index 391fa9f7..427b9c14 100644
--- a/auth_server/server/config.go
+++ b/auth_server/server/config.go
@@ -48,6 +48,7 @@ type Config struct {
type ServerConfig struct {
ListenAddress string `yaml:"addr,omitempty"`
+ PathPrefix string `yaml:"path_prefix,omitempty"`
RealIPHeader string `yaml:"real_ip_header,omitempty"`
RealIPPos int `yaml:"real_ip_pos,omitempty"`
CertFile string `yaml:"certificate,omitempty"`
@@ -78,6 +79,9 @@ func validate(c *Config) error {
if c.Server.ListenAddress == "" {
return errors.New("server.addr is required")
}
+ if c.Server.PathPrefix != "" && !strings.HasPrefix(c.Server.PathPrefix, "/") {
+ return errors.New("server.path_prefix must be an absolute path")
+ }
if c.Token.Issuer == "" {
return errors.New("token.issuer is required")
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 5f0fa24c..75dffba9 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -332,14 +332,15 @@ func (as *AuthServer) CreateToken(ar *authRequest, ares []authzResult) (string,
func (as *AuthServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
glog.V(3).Infof("Request: %+v", req)
+ path_prefix := as.config.Server.PathPrefix
switch {
- case req.URL.Path == "/":
+ case req.URL.Path == path_prefix + "/":
as.doIndex(rw, req)
- case req.URL.Path == "/auth":
+ case req.URL.Path == path_prefix + "/auth":
as.doAuth(rw, req)
- case req.URL.Path == "/google_auth" && as.ga != nil:
+ case req.URL.Path == path_prefix + "/google_auth" && as.ga != nil:
as.ga.DoGoogleAuth(rw, req)
- case req.URL.Path == "/github_auth" && as.gha != nil:
+ case req.URL.Path == path_prefix + "/github_auth" && as.gha != nil:
as.gha.DoGitHubAuth(rw, req)
default:
http.Error(rw, "Not found", http.StatusNotFound)
diff --git a/examples/reference.yml b/examples/reference.yml
index 2cf4957a..839f8c68 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -13,6 +13,9 @@ server: # Server settings.
# Address to listen on.
addr: ":5001"
+ # URL path prefix to use.
+ path_prefix: ""
+
# TLS options.
#
# Use specific certificate and key.
From 87d796033c70c8a3366db2235ac7b29cff752ce3 Mon Sep 17 00:00:00 2001
From: Mark Ide
Date: Tue, 18 Jul 2017 15:07:04 -0400
Subject: [PATCH 069/209] Adds a note regarding Third-Party Cookies
When using docker_auth with Google or GitHub (OAuth)
authentication, the browser will not be able to complete
the request if third-party cookies are blocked
---
examples/reference.yml | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/examples/reference.yml b/examples/reference.yml
index 839f8c68..0271f8bb 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -73,7 +73,8 @@ google_auth:
domain: "example.com" # Optional. If set, only logins from this domain are accepted.
# client_id and client_secret for API access. Required.
# Follow instructions here: https://developers.google.com/identity/sign-in/web/devconsole-project
- # NB: Make sure JavaScript origins are configured correctly.
+ # NB: Make sure JavaScript origins are configured correctly, and that third-party
+ # cookies are not blocked in the browser being used to login.
client_id: "1223123456-somethingsomething.apps.googleusercontent.com"
# Either client_secret or client_secret_file is required. Use client_secret_file if you don't
# want to have sensitive information checked in.
@@ -93,7 +94,8 @@ github_auth:
organization: "acme" # Optional. If set, only logins from this organization are accepted.
# client_id and client_secret for API access. Required.
# You can register a new application here: https://github.com/settings/developers
- # NB: Make sure JavaScript origins are configured correctly.
+ # NB: Make sure JavaScript origins are configured correctly, and that third-party
+ # cookies are not blocked in the browser being used to login.
client_id: "1223123456"
# Either client_secret or client_secret_file is required. Use client_secret_file if you don't
# want to have sensitive information checked in.
From dcf6adb8a2015c56b3a693da47994287095bc908 Mon Sep 17 00:00:00 2001
From: Miroslav Genov
Date: Mon, 7 Aug 2017 14:31:48 +0300
Subject: [PATCH 070/209] docker_auth/github: pass read:org scope for the
authorization (#190)
The read:org scope is required for the authorization as it allows user
to check whether it's a member of the organization or not.
GitHub Scopes for OAuth Apps are described:
https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-scopes-for-oauth-apps/
Fixes #189
---
auth_server/authn/data/github_auth.tmpl | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/auth_server/authn/data/github_auth.tmpl b/auth_server/authn/data/github_auth.tmpl
index 05daa798..67529e99 100644
--- a/auth_server/authn/data/github_auth.tmpl
+++ b/auth_server/authn/data/github_auth.tmpl
@@ -1,6 +1,6 @@
-
+
From 7a39a9fd713bdc77cbc03349501dd8060c7b17a2 Mon Sep 17 00:00:00 2001
From: Miroslav Genov
Date: Mon, 7 Aug 2017 19:27:10 +0300
Subject: [PATCH 071/209] doc: document label usage in reference.yml
Added label usage examples in the reference.yml.
Fixes #188
---
examples/reference.yml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/examples/reference.yml b/examples/reference.yml
index 0271f8bb..b8bb08ca 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -229,6 +229,12 @@ acl:
- match: {account: "/^(.+)@test.com$/", name: "${account:1}/*"}
actions: []
comment: "Emit domain part of account to make it a correct repo name"
+ - match: {labels: {"group": "VIP"}}
+ actions: ["push"]
+ comment: "Users assigned to group 'VIP' is able to push"
+ - match: {labels: {"group": "/trainee|dev/"}}
+ actions: ["push", "pull"]
+ comment: "Users assigned to group 'trainee' and 'dev' is able to push and pull"
# Access is denied by default.
# (optional) Define to query ACL from a MongoDB server.
From a213b075c474b33eae8ccf93c5edd9765561dec7 Mon Sep 17 00:00:00 2001
From: Carson Anderson
Date: Tue, 29 Aug 2017 14:15:32 -0600
Subject: [PATCH 072/209] Return labels from mongo users
---
auth_server/authn/mongo_auth.go | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/auth_server/authn/mongo_auth.go b/auth_server/authn/mongo_auth.go
index 72d08689..c6aee896 100644
--- a/auth_server/authn/mongo_auth.go
+++ b/auth_server/authn/mongo_auth.go
@@ -43,6 +43,7 @@ type MongoAuth struct {
type authUserEntry struct {
Username *string `yaml:"username,omitempty" json:"username,omitempty"`
Password *string `yaml:"password,omitempty" json:"password,omitempty"`
+ Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"`
}
func NewMongoAuth(c *MongoAuthConfig) (*MongoAuth, error) {
@@ -84,19 +85,19 @@ func NewMongoAuth(c *MongoAuthConfig) (*MongoAuth, error) {
func (mauth *MongoAuth) Authenticate(account string, password PasswordString) (bool, Labels, error) {
for true {
- result, err := mauth.authenticate(account, password)
+ result, labels, err := mauth.authenticate(account, password)
if err == io.EOF {
glog.Warningf("EOF error received from Mongo. Retrying connection")
time.Sleep(time.Second)
continue
}
- return result, nil, err
+ return result, labels, err
}
return false, nil, errors.New("Unable to communicate with Mongo.")
}
-func (mauth *MongoAuth) authenticate(account string, password PasswordString) (bool, error) {
+func (mauth *MongoAuth) authenticate(account string, password PasswordString) (bool, Labels, error) {
// Copy our session
tmp_session := mauth.session.Copy()
// Close up when we are done
@@ -111,20 +112,20 @@ func (mauth *MongoAuth) authenticate(account string, password PasswordString) (b
// If we connect and get no results we return a NoMatch so auth can fall-through
if err == mgo.ErrNotFound {
- return false, NoMatch
+ return false, nil, NoMatch
} else if err != nil {
- return false, err
+ return false, nil, err
}
// Validate db password against passed password
if dbUserRecord.Password != nil {
if bcrypt.CompareHashAndPassword([]byte(*dbUserRecord.Password), []byte(password)) != nil {
- return false, nil
+ return false, nil, nil
}
}
// Auth success
- return true, nil
+ return true, dbUserRecord.Labels, nil
}
// Validate ensures that any custom config options
From 4e49efe8986425eb52f5494d94d29b512dfa5bc3 Mon Sep 17 00:00:00 2001
From: Miroslav Genov
Date: Tue, 15 Aug 2017 20:21:12 +0300
Subject: [PATCH 073/209] docker_auth/github: store tokens in google cloud
storage
Added a new implementation of TokenDB that uses Google Cloud Storage as
backend for storing of the tokens.
This makes container independent from the file system of the container
or from the target system and is good alternative to mounted clustered
file systems to container.
Docs examples are updated regarding this change as specifying token_db
configuration now becomes alternate between local file and gcs.
---
auth_server/authn/github_auth.go | 37 +++++++---
auth_server/authn/tokendb.go | 4 +-
auth_server/authn/tokendb_gcs.go | 121 +++++++++++++++++++++++++++++++
auth_server/server/config.go | 8 +-
auth_server/vendor/vendor.json | 14 +++-
examples/reference.yml | 6 +-
6 files changed, 173 insertions(+), 17 deletions(-)
create mode 100644 auth_server/authn/tokendb_gcs.go
diff --git a/auth_server/authn/github_auth.go b/auth_server/authn/github_auth.go
index 0ce9c5ce..6a47aa53 100644
--- a/auth_server/authn/github_auth.go
+++ b/auth_server/authn/github_auth.go
@@ -32,15 +32,21 @@ import (
)
type GitHubAuthConfig struct {
- Organization string `yaml:"organization,omitempty"`
- ClientId string `yaml:"client_id,omitempty"`
- ClientSecret string `yaml:"client_secret,omitempty"`
- ClientSecretFile string `yaml:"client_secret_file,omitempty"`
- TokenDB string `yaml:"token_db,omitempty"`
- HTTPTimeout time.Duration `yaml:"http_timeout,omitempty"`
- RevalidateAfter time.Duration `yaml:"revalidate_after,omitempty"`
- GithubWebUri string `yaml:"github_web_uri,omitempty"`
- GithubApiUri string `yaml:"github_api_uri,omitempty"`
+ Organization string `yaml:"organization,omitempty"`
+ ClientId string `yaml:"client_id,omitempty"`
+ ClientSecret string `yaml:"client_secret,omitempty"`
+ ClientSecretFile string `yaml:"client_secret_file,omitempty"`
+ TokenDB string `yaml:"token_db,omitempty"`
+ GCSTokenDB *GitHubGCSStoreConfig `yaml:"gcs_token_db,omitempty"`
+ HTTPTimeout time.Duration `yaml:"http_timeout,omitempty"`
+ RevalidateAfter time.Duration `yaml:"revalidate_after,omitempty"`
+ GithubWebUri string `yaml:"github_web_uri,omitempty"`
+ GithubApiUri string `yaml:"github_api_uri,omitempty"`
+}
+
+type GitHubGCSStoreConfig struct {
+ Bucket string `yaml:"bucket,omitempty"`
+ ClientSecretFile string `yaml:"client_secret_file,omitempty"`
}
type GitHubAuthRequest struct {
@@ -62,11 +68,20 @@ type GitHubAuth struct {
}
func NewGitHubAuth(c *GitHubAuthConfig) (*GitHubAuth, error) {
- db, err := NewTokenDB(c.TokenDB)
+ var db TokenDB
+ var err error
+ dbName := c.TokenDB
+ if c.GCSTokenDB == nil {
+ db, err = NewTokenDB(c.TokenDB)
+ } else {
+ db, err = NewGCSTokenDB(c.GCSTokenDB.Bucket, c.GCSTokenDB.ClientSecretFile)
+ dbName = "GCS: " + c.GCSTokenDB.Bucket
+ }
+
if err != nil {
return nil, err
}
- glog.Infof("GitHub auth token DB at %s", c.TokenDB)
+ glog.Infof("GitHub auth token DB at %s", dbName)
return &GitHubAuth{
config: c,
db: db,
diff --git a/auth_server/authn/tokendb.go b/auth_server/authn/tokendb.go
index a7d8bf0a..6f650c57 100644
--- a/auth_server/authn/tokendb.go
+++ b/auth_server/authn/tokendb.go
@@ -24,8 +24,8 @@ import (
"golang.org/x/crypto/bcrypt"
- "github.com/dchest/uniuri"
"github.com/cesanta/glog"
+ "github.com/dchest/uniuri"
"github.com/syndtr/goleveldb/leveldb"
)
@@ -93,7 +93,7 @@ func (db *TokenDBImpl) GetValue(user string) (*TokenDBValue, error) {
err = json.Unmarshal(valueStr, &dbv)
if err != nil {
glog.Errorf("bad DB value for %q (%q): %s", user, string(valueStr), err)
- return nil, fmt.Errorf("bad DB value", err)
+ return nil, fmt.Errorf("bad DB value due: %v", err)
}
return &dbv, nil
}
diff --git a/auth_server/authn/tokendb_gcs.go b/auth_server/authn/tokendb_gcs.go
new file mode 100644
index 00000000..68158ff1
--- /dev/null
+++ b/auth_server/authn/tokendb_gcs.go
@@ -0,0 +1,121 @@
+/*
+ Copyright 2017 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 (
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "cloud.google.com/go/storage"
+ "github.com/cesanta/glog"
+ "github.com/dchest/uniuri"
+ "golang.org/x/crypto/bcrypt"
+ "golang.org/x/net/context"
+ "google.golang.org/api/option"
+)
+
+// NewGCSTokenDB return a new TokenDB structure which uses Google Cloud Storage as backend. The
+// created DB uses file-per-user strategy and stores credentials independently for each user.
+//
+// Note: it's not recomanded bucket to be shared with other apps or services
+func NewGCSTokenDB(bucket, clientSecretFile string) (TokenDB, error) {
+ gcs, err := storage.NewClient(context.Background(), option.WithServiceAccountFile(clientSecretFile))
+ return &gcsTokenDB{gcs, bucket}, err
+}
+
+type gcsTokenDB struct {
+ gcs *storage.Client
+ bucket string
+}
+
+// GetValue gets token value associated with the provided user. Each user
+// in the bucket is having it's own file for tokens and it's recomanded bucket
+// to not be shared with other apps
+func (db *gcsTokenDB) GetValue(user string) (*TokenDBValue, error) {
+ rd, err := db.gcs.Bucket(db.bucket).Object(user).NewReader(context.Background())
+ if err == storage.ErrObjectNotExist {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("could not retrieved token for user '%s' due: %v", user, err)
+ }
+ defer rd.Close()
+
+ var dbv TokenDBValue
+ if err := json.NewDecoder(rd).Decode(&dbv); err != nil {
+ glog.Errorf("bad DB value for %q: %v", user, err)
+ return nil, fmt.Errorf("could not read token for user '%s' due: %v", user, err)
+ }
+
+ return &dbv, nil
+}
+
+// StoreToken stores token in the GCS file in a JSON format. Note that separate file is
+// used for each user
+func (db *gcsTokenDB) StoreToken(user string, v *TokenDBValue, updatePassword bool) (dp string, err error) {
+ if updatePassword {
+ dp = uniuri.New()
+ dph, _ := bcrypt.GenerateFromPassword([]byte(dp), bcrypt.DefaultCost)
+ v.DockerPassword = string(dph)
+ }
+
+ wr := db.gcs.Bucket(db.bucket).Object(user).NewWriter(context.Background())
+
+ if err := json.NewEncoder(wr).Encode(v); err != nil {
+ glog.Errorf("failed to set token data for %s: %s", user, err)
+ return "", fmt.Errorf("failed to set token data for %s due: %v", user, err)
+ }
+
+ err = wr.Close()
+ return
+}
+
+// ValidateToken verifies whether the provided token passed as password field
+// is still valid, e.g available and not expired
+func (db *gcsTokenDB) ValidateToken(user string, password PasswordString) error {
+ dbv, err := db.GetValue(user)
+ if err != nil {
+ return err
+ }
+ if dbv == nil {
+ return NoMatch
+ }
+
+ if bcrypt.CompareHashAndPassword([]byte(dbv.DockerPassword), []byte(password)) != nil {
+ return WrongPass
+ }
+ if time.Now().After(dbv.ValidUntil) {
+ return ExpiredToken
+ }
+
+ return nil
+}
+
+// DeleteToken deletes the GCS file that is associated with the provided user.
+func (db *gcsTokenDB) DeleteToken(user string) error {
+ ctx := context.Background()
+ err := db.gcs.Bucket(db.bucket).Object(user).Delete(ctx)
+ if err == storage.ErrObjectNotExist {
+ return nil
+ }
+ return err
+}
+
+// Close is a nop operation for this db
+func (db *gcsTokenDB) Close() error {
+ return nil
+}
diff --git a/auth_server/server/config.go b/auth_server/server/config.go
index 427b9c14..fe08c6d0 100644
--- a/auth_server/server/config.go
+++ b/auth_server/server/config.go
@@ -120,8 +120,12 @@ func validate(c *Config) error {
}
ghac.ClientSecret = strings.TrimSpace(string(contents))
}
- if ghac.ClientId == "" || ghac.ClientSecret == "" || ghac.TokenDB == "" {
- return errors.New("github_auth.{client_id,client_secret,token_db} are required.")
+ if ghac.ClientId == "" || ghac.ClientSecret == "" || (ghac.TokenDB == "" && ghac.GCSTokenDB == nil) {
+ return errors.New("github_auth.{client_id,client_secret,token_db} are required")
+ }
+
+ if ghac.ClientId == "" || ghac.ClientSecret == "" || (ghac.GCSTokenDB != nil && (ghac.GCSTokenDB.Bucket == "" || ghac.GCSTokenDB.ClientSecretFile == "")) {
+ return errors.New("github_auth.{client_id,client_secret,gcs_token_db{bucket,client_secret_file}} are required")
}
if ghac.HTTPTimeout <= 0 {
ghac.HTTPTimeout = time.Duration(10 * time.Second)
diff --git a/auth_server/vendor/vendor.json b/auth_server/vendor/vendor.json
index f261b9f2..9bfddc3c 100644
--- a/auth_server/vendor/vendor.json
+++ b/auth_server/vendor/vendor.json
@@ -2,6 +2,12 @@
"comment": "",
"ignore": "",
"package": [
+ {
+ "checksumSHA1": "0ot8Hk23WGrT0lE2BmQXeqe4bRo=",
+ "path": "cloud.google.com/go/storage",
+ "revision": "2b74e2e25316cfd9e46b74e444cdeceb78786dc5",
+ "revisionTime": "2017-08-20T12:51:33Z"
+ },
{
"checksumSHA1": "CujWu7+PWlZSX5+zAPJH91O5AVQ=",
"origin": "github.com/docker/distribution/vendor/github.com/Sirupsen/logrus",
@@ -397,11 +403,17 @@
"revisionTime": "2017-03-21T17:14:25Z"
},
{
- "checksumSHA1": "AK65RmsGNBl0/e11OVrf2mW78gU=",
+ "checksumSHA1": "1WoWjPiwUEFahi5xz29FRMtd8sA=",
"path": "golang.org/x/sys/unix",
"revision": "493114f68206f85e7e333beccfabc11e98cba8dd",
"revisionTime": "2017-03-31T21:25:38Z"
},
+ {
+ "checksumSHA1": "RpAaByicZuXzN7bReX8YXKf8gP0=",
+ "path": "google.golang.org/api/option",
+ "revision": "955a3ae66b420f3adc0d77da3d8ed767a74e2b4f",
+ "revisionTime": "2017-09-01T00:04:07Z"
+ },
{
"checksumSHA1": "fRERF7JFq7KYgM9I48onMlEgFm4=",
"path": "gopkg.in/asn1-ber.v1",
diff --git a/examples/reference.yml b/examples/reference.yml
index b8bb08ca..61638fdd 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -101,8 +101,12 @@ github_auth:
# want to have sensitive information checked in.
# client_secret: "verysecret"
client_secret_file: "/path/to/client_secret.txt"
- # Where to store server tokens. Required.
+ # Either token_db file for storing of server tokens.
token_db: "/somewhere/to/put/github_tokens.ldb"
+ # or google cloud storage for storing of the sensitive information.
+ gcs_token_db:
+ bucket: "tokenBucket"
+ client_secret_file: "/path/to/client_secret.json"
# How long to wait when talking to GitHub servers. Optional.
http_timeout: "10s"
# How long to wait before revalidating the GitHub token. Optional.
From aea8fdcdf3d126882c6f93ec7ce38273cfe4ad5b Mon Sep 17 00:00:00 2001
From: Carson Anderson
Date: Tue, 29 Aug 2017 16:34:05 -0600
Subject: [PATCH 074/209] Add matching of label placeholders
---
auth_server/authz/acl.go | 53 ++++++++++++++++++++--
auth_server/authz/acl_test.go | 16 +++++++
auth_server/vendor/vendor.json | 8 +++-
docs/Backend_MongoDB.md | 23 ++++++++--
docs/Labels.md | 81 ++++++++++++++++++++++++++++++++++
examples/reference.yml | 10 +++++
6 files changed, 182 insertions(+), 9 deletions(-)
create mode 100644 docs/Labels.md
diff --git a/auth_server/authz/acl.go b/auth_server/authz/acl.go
index 19d36a91..7404a72f 100644
--- a/auth_server/authz/acl.go
+++ b/auth_server/authz/acl.go
@@ -12,6 +12,7 @@ import (
"github.com/cesanta/docker_auth/auth_server/authn"
"github.com/cesanta/glog"
+ "github.com/schwarmco/go-cartesian-product"
)
type ACL []ACLEntry
@@ -153,6 +154,42 @@ func matchString(pp *string, s string, vars []string) bool {
return err == nil && matched
}
+func matchStringWithLabelPermutations(pp *string, s string, vars []string, labelMap *map[string][]string) bool {
+ var matched bool
+ // First try basic matching
+ matched = matchString(pp, s, vars)
+ // If basic matching fails then try with label permuations
+ if !matched {
+ // Take the labelMap and build the structure required for the cartesian library
+ var labelSets [][]interface{}
+ for placeholder, labels := range *labelMap {
+ // Don't bother generating perumations for placeholders not in match string
+ // Since the label permuations are a cartesian product this can have
+ // a huge impact on performance
+ if strings.Contains(*pp, placeholder) {
+ var labelSet []interface{}
+ for _, label := range labels {
+ labelSet = append(labelSet, []string{placeholder, label})
+ }
+ labelSets = append(labelSets, labelSet)
+ }
+ }
+ if len(labelSets) > 0 {
+ for permuation := range cartesian.Iter(labelSets...) {
+ var labelVars []string
+ for _, val := range permuation {
+ labelVars = append(labelVars, val.([]string)...)
+ }
+ matched = matchString(pp, s, append(vars, labelVars...))
+ if matched {
+ break
+ }
+ }
+ }
+ }
+ return matched
+}
+
func matchIP(ipp *string, ip net.IP) bool {
if ipp == nil {
return true
@@ -233,10 +270,18 @@ func (mc *MatchConditions) Matches(ai *AuthRequestInfo) bool {
vars = append(vars, found[0], text[index])
}
}
- return matchString(mc.Account, ai.Account, vars) &&
- matchString(mc.Type, ai.Type, vars) &&
- matchString(mc.Name, ai.Name, vars) &&
- matchString(mc.Service, ai.Service, vars) &&
+ labelMap := make(map[string][]string)
+ for label, labelValues := range ai.Labels {
+ var labelSet []string
+ for _, lv := range labelValues {
+ labelSet = append(labelSet, regexp.QuoteMeta(lv))
+ }
+ labelMap[fmt.Sprintf("${labels:%s}", label)] = labelSet
+ }
+ return matchStringWithLabelPermutations(mc.Account, ai.Account, vars, &labelMap) &&
+ matchStringWithLabelPermutations(mc.Type, ai.Type, vars, &labelMap) &&
+ matchStringWithLabelPermutations(mc.Name, ai.Name, vars, &labelMap) &&
+ matchStringWithLabelPermutations(mc.Service, ai.Service, vars, &labelMap) &&
matchIP(mc.IP, ai.IP) &&
matchLabels(mc.Labels, ai.Labels, vars)
}
diff --git a/auth_server/authz/acl_test.go b/auth_server/authz/acl_test.go
index 4e6b2477..3a8bd31d 100644
--- a/auth_server/authz/acl_test.go
+++ b/auth_server/authz/acl_test.go
@@ -58,6 +58,12 @@ func TestMatching(t *testing.T) {
ai1 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz", Service: "notary"}
ai2 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz", Service: "notary",
Labels: map[string][]string{"group": []string{"admins", "VIP"}}}
+ ai3 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "admins/foo", Service: "notary",
+ Labels: map[string][]string{"group": []string{"admins", "VIP"}}}
+ ai4 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "VIP/api", Service: "notary",
+ Labels: map[string][]string{"group": []string{"admins", "VIP"}, "project": []string{"api", "frontend"}}}
+ ai5 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "devs/api", Service: "notary",
+ Labels: map[string][]string{"group": []string{"admins", "VIP"}, "project": []string{"api", "frontend"}}}
cases := []struct {
mc MatchConditions
ai AuthRequestInfo
@@ -99,6 +105,16 @@ func TestMatching(t *testing.T) {
{MatchConditions{Labels: map[string]string{"group": "VIP"}}, ai2, true},
{MatchConditions{Labels: map[string]string{"group": "a*"}}, ai2, true},
{MatchConditions{Labels: map[string]string{"group": "/(admins|VIP)/"}}, ai2, true},
+ // // Label placeholder matching
+ {MatchConditions{Name: sp("${labels:group}/*")}, ai1, false}, // no labels
+ {MatchConditions{Name: sp("${labels:noexist}/*")}, ai2, false}, // wrong labels
+ {MatchConditions{Name: sp("${labels:group}/*")}, ai3, true}, // match label
+ {MatchConditions{Name: sp("${labels:noexist}/*")}, ai3, false}, // missing label
+ {MatchConditions{Name: sp("${labels:group}/${labels:project}")}, ai4, true}, // multiple label match success
+ {MatchConditions{Name: sp("${labels:group}/${labels:noexist}")}, ai4, false}, // multiple label match fail
+ {MatchConditions{Name: sp("${labels:group}/${labels:project}")}, ai4, true}, // multiple label match success
+ {MatchConditions{Name: sp("${labels:group}/${labels:noexist}")}, ai4, false}, // multiple label match fail wrong label
+ {MatchConditions{Name: sp("${labels:group}/${labels:project}")}, ai5, false}, // multiple label match fail. right label, wrong value
}
for i, c := range cases {
if result := c.mc.Matches(&c.ai); result != c.matches {
diff --git a/auth_server/vendor/vendor.json b/auth_server/vendor/vendor.json
index f261b9f2..f63520b0 100644
--- a/auth_server/vendor/vendor.json
+++ b/auth_server/vendor/vendor.json
@@ -293,6 +293,12 @@
"revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
"revisionTime": "2015-05-30T21:13:11Z"
},
+ {
+ "checksumSHA1": "DVgRSBT6UzmEgC90yJhh6X5A7Yc=",
+ "path": "github.com/schwarmco/go-cartesian-product",
+ "revision": "c2c0aca869a6cbf51e017ce148b949d9dee09bc3",
+ "revisionTime": "2017-01-30T17:09:49Z"
+ },
{
"checksumSHA1": "GVY3lzvj4xmpKOGgA4/h9GWjQVk=",
"path": "github.com/syndtr/goleveldb/leveldb",
@@ -397,7 +403,7 @@
"revisionTime": "2017-03-21T17:14:25Z"
},
{
- "checksumSHA1": "AK65RmsGNBl0/e11OVrf2mW78gU=",
+ "checksumSHA1": "1WoWjPiwUEFahi5xz29FRMtd8sA=",
"path": "golang.org/x/sys/unix",
"revision": "493114f68206f85e7e333beccfabc11e98cba8dd",
"revisionTime": "2017-03-31T21:25:38Z"
diff --git a/docs/Backend_MongoDB.md b/docs/Backend_MongoDB.md
index 54a13e51..5a322ead 100644
--- a/docs/Backend_MongoDB.md
+++ b/docs/Backend_MongoDB.md
@@ -10,12 +10,21 @@ which can query ACL and Auth from a MongoDB database.
## Auth backend in MongoDB
Auth entries in mongo are single dictionary containing a username and password entry.
-The password entry must contain a BCrypt hash.
+The password entry must contain a BCrypt hash. The labels entry is optional.
```json
{
"username" : "admin",
- "password" : "$2y$05$B.x046DV3bvuwFgn0I42F.W/SbRU5fUoCbCGtjFl7S33aCUHNBxbq"
+ "password" : "$2y$05$B.x046DV3bvuwFgn0I42F.W/SbRU5fUoCbCGtjFl7S33aCUHNBxbq",
+ "labels" : {
+ "group" : [
+ "dev"
+ ],
+ "project": [
+ "website",
+ "api"
+ ]
+ }
}
```
@@ -43,15 +52,21 @@ guarantee by default, i.e. [Natural Sorting](https://docs.mongodb.org/manual/ref
``seq`` is a required field in all MongoDB ACL documents. Any documents without this key will be excluded. seq uniqeness is also enforced.
-**reference_acl.json**
+ - match: {labels: {"group": "/trainee|dev/"}}
+ actions: ["push", "pull"]
+ comment: "Users assigned to group 'trainee' and 'dev' is able to push and pull"
+**reference_acl.json**
```json
{"seq": 10, "match" : {"account" : "admin"}, "actions" : ["*"], "comment" : "Admin has full access to everything."}
+{"seq": 11, "match" : {"labels": {"group": "admin"}}, "actions" : ["*"], "comment" : "Admin group members have full access to everything"}
{"seq": 20, "match" : {"account" : "test", "name" : "test-*"}, "actions" : ["*"], "comment" : "User \"test\" has full access to test-* images but nothing else. (1)"}
{"seq": 30, "match" : {"account" : "test"}, "actions" : [], "comment" : "User \"test\" has full access to test-* images but nothing else. (2)"}
{"seq": 40, "match" : {"account" : "/.+/"}, "actions" : ["pull"], "comment" : "All logged in users can pull all images."}
{"seq": 50, "match" : {"account" : "/.+/", "name" : "${account}/*"}, "actions" : ["*"], "comment" : "All logged in users can push all images that are in a namespace beginning with their name"}
-{"seq": 60, "match" : {"account" : "", "name" : "hello-world"}, "actions" : ["pull"], "comment" : "Anonymous users can pull \"hello-world\"."}
+{"seq": 60, "match" : {"name" : "${labels:group}-shared/*"}, "actions" : ["push", "pull"], "comment" : "Users can pull and push to the shared namespace of any group they are in"}
+{"seq": 70, "match" : {"name" : "${labels:project}/*"}, "actions" : ["push", "pull"], "comment" : "Users can pull and push to to namespaces matching projects they are assigned to"}
+{"seq": 80, "match" : {"account" : "", "name" : "hello-world"}, "actions" : ["pull"], "comment" : "Anonymous users can pull \"hello-world\"."}
```
**Note** that each document entry must span exactly one line or otherwise the
diff --git a/docs/Labels.md b/docs/Labels.md
new file mode 100644
index 00000000..34b3a468
--- /dev/null
+++ b/docs/Labels.md
@@ -0,0 +1,81 @@
+# Labels
+
+Labels can be used to reduce the number ACLS needed in large, complex installations.
+
+## Label Placeholders
+
+Label placeholders are available for any label that is assigned to a user.
+
+For example, given a user:
+
+```json
+{
+ "username" : "busy-guy",
+ "password" : "$2y$05$B.x046DV3bvuwFgn0I42F.W/SbRU5fUoCbCGtjFl7S33aCUHNBxbq",
+ "labels" : {
+ "group" : [
+ "web",
+ "webdev"
+ ],
+ "project" : [
+ "website",
+ "api"
+ ],
+ "tier" : [
+ "frontend",
+ "backend"
+ ]
+ }
+}
+```
+
+The following placeholders could be used in any match field:
+
+ * `${labels:group}`
+ * `${labels:project}`
+ * `${labels:tier}`
+
+Example acl with label matching:
+
+```json
+{
+ "match": { "name": "${labels:project}/*" },
+ "actions": [ "push", "pull" ],
+ "comment": "Users can push to any project they are assigned to"
+}
+```
+
+Single label matching is efficient and will be tested in the order
+they are listed in the user record.
+
+
+## Using Multiple Labels when matching
+
+It's possible to use multiple labels in a single match. When multiple labels are
+used in a single match all possible combinations of the labels are tested
+in [no particular order](https://blog.golang.org/go-maps-in-action#TOC_7.).
+
+Example acl with multiple label matching:
+
+```json
+{
+ "match": { "name": "${labels:project}/${labels:group}-${labels:tier}" },
+ "actions": [ "push", "pull" ],
+ "comment": "Contrived multiple label match rule"
+}
+```
+
+When paired with the user given above would result in 8 possible combinations
+that would need to be tested.
+
+ * `${labels:project} : website`, `${labels:group} : dev`, `${labels:tier} : frontend`
+ * `${labels:project} : website`, `${labels:group} : dev`, `${labels:tier} : backend`
+ * `${labels:project} : website`, `${labels:group} : webdev`, `${labels:tier} : frontend`
+ * `${labels:project} : website`, `${labels:group} : webdev`, `${labels:tier} : backend`
+ * `${labels:project} : api`, `${labels:group} : dev`, `${labels:tier} : frontend`
+ * `${labels:project} : api`, `${labels:group} : dev`, `${labels:tier} : backend`
+ * `${labels:project} : api`, `${labels:group} : webdev`, `${labels:tier} : frontend`
+ * `${labels:project} : api`, `${labels:group} : webdev`, `${labels:tier} : backend`
+
+This grows rapidly as more placeholders and labels are added. So it's best
+to limit multiple label matching when possible.
diff --git a/examples/reference.yml b/examples/reference.yml
index b8bb08ca..54b83622 100644
--- a/examples/reference.yml
+++ b/examples/reference.yml
@@ -195,6 +195,7 @@ ext_auth:
# * ${service} - the service name, specified by auth.token.service in the registry config.
# * ${type} - the type of the entity, normally "repository".
# * ${name} - the name of the repository (i.e. image), e.g. centos.
+# * ${labels:
- $ docker login -u {{.Username}} -p {{.Password}} YOUR_REGISTRY_FQDN
+ $ docker login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}
`)
@@ -195,7 +201,7 @@ func dataGithub_auth_resultTmpl() (*asset, error) {
return nil, err
}
- info := bindataFileInfo{name: "data/github_auth_result.tmpl", size: 1094, mode: os.FileMode(420), modTime: time.Unix(1, 0)}
+ info := bindataFileInfo{name: "data/github_auth_result.tmpl", size: 1300, mode: os.FileMode(420), modTime: time.Unix(1, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
diff --git a/auth_server/authn/data/github_auth_result.tmpl b/auth_server/authn/data/github_auth_result.tmpl
index df73761d..2619d0cd 100644
--- a/auth_server/authn/data/github_auth_result.tmpl
+++ b/auth_server/authn/data/github_auth_result.tmpl
@@ -32,6 +32,12 @@
border-radius: 0.5em;
text-shadow: 0px 1px 0px #fff;
}
+ .command span {
+ user-select: none;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ }
@@ -40,6 +46,6 @@
Use the following username and password to login into the registry:
- $ docker login -u {{.Username}} -p {{.Password}} YOUR_REGISTRY_FQDN
+ $ docker login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}