1
2
3
4
5
6 package htpasswd
7
8 import (
9 "context"
10 "crypto/rand"
11 "encoding/base64"
12 "fmt"
13 "net/http"
14 "os"
15 "path/filepath"
16 "sync"
17 "time"
18
19 "golang.org/x/crypto/bcrypt"
20
21 dcontext "github.com/docker/distribution/context"
22 "github.com/docker/distribution/registry/auth"
23 )
24
25 type accessController struct {
26 realm string
27 path string
28 modtime time.Time
29 mu sync.Mutex
30 htpasswd *htpasswd
31 }
32
33 var _ auth.AccessController = &accessController{}
34
35 func newAccessController(options map[string]interface{}) (auth.AccessController, error) {
36 realm, present := options["realm"]
37 if _, ok := realm.(string); !present || !ok {
38 return nil, fmt.Errorf(`"realm" must be set for htpasswd access controller`)
39 }
40
41 pathOpt, present := options["path"]
42 path, ok := pathOpt.(string)
43 if !present || !ok {
44 return nil, fmt.Errorf(`"path" must be set for htpasswd access controller`)
45 }
46 if err := createHtpasswdFile(path); err != nil {
47 return nil, err
48 }
49 return &accessController{realm: realm.(string), path: path}, nil
50 }
51
52 func (ac *accessController) Authorized(ctx context.Context, accessRecords ...auth.Access) (context.Context, error) {
53 req, err := dcontext.GetRequest(ctx)
54 if err != nil {
55 return nil, err
56 }
57
58 username, password, ok := req.BasicAuth()
59 if !ok {
60 return nil, &challenge{
61 realm: ac.realm,
62 err: auth.ErrInvalidCredential,
63 }
64 }
65
66
67 fstat, err := os.Stat(ac.path)
68 if err != nil {
69 return nil, err
70 }
71
72 lastModified := fstat.ModTime()
73 ac.mu.Lock()
74 if ac.htpasswd == nil || !ac.modtime.Equal(lastModified) {
75 ac.modtime = lastModified
76
77 f, err := os.Open(ac.path)
78 if err != nil {
79 ac.mu.Unlock()
80 return nil, err
81 }
82 defer f.Close()
83
84 h, err := newHTPasswd(f)
85 if err != nil {
86 ac.mu.Unlock()
87 return nil, err
88 }
89 ac.htpasswd = h
90 }
91 localHTPasswd := ac.htpasswd
92 ac.mu.Unlock()
93
94 if err := localHTPasswd.authenticateUser(username, password); err != nil {
95 dcontext.GetLogger(ctx).Errorf("error authenticating user %q: %v", username, err)
96 return nil, &challenge{
97 realm: ac.realm,
98 err: auth.ErrAuthenticationFailure,
99 }
100 }
101
102 return auth.WithUser(ctx, auth.UserInfo{Name: username}), nil
103 }
104
105
106 type challenge struct {
107 realm string
108 err error
109 }
110
111 var _ auth.Challenge = challenge{}
112
113
114 func (ch challenge) SetHeaders(r *http.Request, w http.ResponseWriter) {
115 w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", ch.realm))
116 }
117
118 func (ch challenge) Error() string {
119 return fmt.Sprintf("basic authentication challenge for realm %q: %s", ch.realm, ch.err)
120 }
121
122
123 func createHtpasswdFile(path string) error {
124 if f, err := os.Open(path); err == nil {
125 f.Close()
126 return nil
127 } else if !os.IsNotExist(err) {
128 return err
129 }
130
131 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
132 return err
133 }
134 f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600)
135 if err != nil {
136 return fmt.Errorf("failed to open htpasswd path %s", err)
137 }
138 defer f.Close()
139 var secretBytes [32]byte
140 if _, err := rand.Read(secretBytes[:]); err != nil {
141 return err
142 }
143 pass := base64.RawURLEncoding.EncodeToString(secretBytes[:])
144 encryptedPass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
145 if err != nil {
146 return err
147 }
148 if _, err := f.Write([]byte(fmt.Sprintf("docker:%s", string(encryptedPass[:])))); err != nil {
149 return err
150 }
151 dcontext.GetLoggerWithFields(context.Background(), map[interface{}]interface{}{
152 "user": "docker",
153 "password": pass,
154 }).Warnf("htpasswd is missing, provisioning with default user")
155 return nil
156 }
157
158 func init() {
159 auth.Register("htpasswd", auth.InitFunc(newAccessController))
160 }
161
View as plain text