From 3b097dc9a9462b65822d9763044fb3c06585ed36 Mon Sep 17 00:00:00 2001 From: rojer Date: Wed, 6 Apr 2016 14:15:36 +0100 Subject: [PATCH 001/188] 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 002/188] 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 003/188] 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 004/188] 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 005/188] 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 006/188] 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 007/188] 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 008/188] 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 009/188] 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 010/188] 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 011/188] 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 012/188] 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 013/188] 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 014/188] 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 015/188] 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 016/188] 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 017/188] 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 018/188] 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 019/188] 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 020/188] 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 021/188] 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 022/188] 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 023/188] 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 024/188] 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 025/188] 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 026/188] 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 027/188] 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 028/188] 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 029/188] 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 030/188] 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 031/188] 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 032/188] 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 033/188] 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 034/188] 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 035/188] 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 036/188] 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 037/188] 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 038/188] 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 039/188] 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 040/188] 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 041/188] 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 042/188] 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 043/188] 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 044/188] 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 045/188] 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 046/188] 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 047/188] 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 048/188] 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 049/188] 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 050/188] 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 051/188] 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 052/188] 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 053/188] 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: