Skip to content

Commit c9f064f

Browse files
committed
Add HTTP authentication backend
Add HTTP authentication library for ratsd. It currently support 3 modes, `passthrough`, `none` and `basic`. `passthrough` backend does not perform any authentication, allowing all requests. `none` is an alias for `passthrough`, and `basic` implements the Basic HTTP authentication scheme defined in RFC7617. Signed-off-by: Ian Chin Wang <[email protected]>
1 parent 39403e3 commit c9f064f

File tree

5 files changed

+248
-0
lines changed

5 files changed

+248
-0
lines changed

Diff for: auth/authorizer.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2025 Contributors to the Veraison project.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package auth
5+
6+
import (
7+
"fmt"
8+
9+
"github.com/spf13/viper"
10+
"github.com/veraison/services/config"
11+
"go.uber.org/zap"
12+
)
13+
14+
type cfg struct {
15+
Backend string `mapstructure:"backend,omitempty"`
16+
BackendConfigs map[string]interface{} `mapstructure:",remain"`
17+
}
18+
19+
func NewAuthorizer(v *viper.Viper, logger *zap.SugaredLogger) (IAuthorizer, error) {
20+
cfg := cfg{
21+
Backend: "passthrough",
22+
}
23+
24+
loader := config.NewLoader(&cfg)
25+
if err := loader.LoadFromViper(v); err != nil {
26+
return nil, err
27+
}
28+
29+
var a IAuthorizer
30+
31+
switch cfg.Backend {
32+
case "none", "passthrough":
33+
a = &PassthroughAuthorizer{}
34+
case "basic":
35+
a = &BasicAuthorizer{}
36+
default:
37+
return nil, fmt.Errorf("backend %q is not supported", cfg.Backend)
38+
}
39+
40+
if err := a.Init(v, logger); err != nil {
41+
return nil, err
42+
}
43+
44+
return a, nil
45+
}

Diff for: auth/basic.go

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2025 Contributors to the Veraison project.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package auth
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"net/http"
10+
11+
"github.com/spf13/viper"
12+
"github.com/veraison/services/log"
13+
"go.uber.org/zap"
14+
"golang.org/x/crypto/bcrypt"
15+
)
16+
17+
type basicAuthUser struct {
18+
PasswordHash string `mapstructure:"password"`
19+
}
20+
21+
func newBasicAuthUser(m map[string]interface{}) (*basicAuthUser, error) {
22+
var newUser basicAuthUser
23+
24+
passRaw, ok := m["password"]
25+
if !ok {
26+
return nil, errors.New("password not set")
27+
}
28+
29+
switch t := passRaw.(type) {
30+
case string:
31+
newUser.PasswordHash = t
32+
default:
33+
return nil, fmt.Errorf("invalid password: expected string found %T", t)
34+
}
35+
36+
return &newUser, nil
37+
}
38+
39+
type BasicAuthorizer struct {
40+
logger *zap.SugaredLogger
41+
users map[string]*basicAuthUser
42+
}
43+
44+
func (o *BasicAuthorizer) Init(v *viper.Viper, logger *zap.SugaredLogger) error {
45+
if logger == nil {
46+
return errors.New("nil logger")
47+
}
48+
o.logger = logger
49+
50+
o.users = make(map[string]*basicAuthUser)
51+
if rawUsers := v.GetStringMap("users"); rawUsers != nil {
52+
for name, rawUser := range rawUsers {
53+
switch t := rawUser.(type) {
54+
case map[string]interface{}:
55+
newUser, err := newBasicAuthUser(t)
56+
if err != nil {
57+
return fmt.Errorf("invalid user %q: %w", name, err)
58+
59+
}
60+
o.logger.Debugw("registered user",
61+
"user", name,
62+
"hashed password", newUser.PasswordHash,
63+
)
64+
o.users[name] = newUser
65+
default:
66+
return fmt.Errorf(
67+
"invalid user %q: expected map[string]interface{}, got %T",
68+
name, t,
69+
)
70+
}
71+
}
72+
}
73+
74+
return nil
75+
}
76+
77+
func (o *BasicAuthorizer) Close() error {
78+
return nil
79+
}
80+
81+
func (o *BasicAuthorizer) GetMiddleware(next http.Handler) http.Handler {
82+
return http.HandlerFunc(
83+
func(w http.ResponseWriter, r *http.Request) {
84+
o.logger.Debugw("auth basic", "path", r.URL.Path)
85+
86+
userName, password, hasAuth := r.BasicAuth()
87+
if !hasAuth {
88+
w.Header().Set("WWW-Authenticate", "Basic realm=veraison")
89+
ReportProblem(o.logger, w, "no Basic Authorizaiton given")
90+
return
91+
}
92+
93+
userInfo, ok := o.users[userName]
94+
if !ok {
95+
w.Header().Set("WWW-Authenticate", "Basic realm=veraison")
96+
ReportProblem(o.logger, w, fmt.Sprintf("no such user: %s", userName))
97+
return
98+
}
99+
100+
if err := bcrypt.CompareHashAndPassword(
101+
[]byte(userInfo.PasswordHash),
102+
[]byte(password),
103+
); err != nil {
104+
o.logger.Debugf("password check failed: %v", err)
105+
w.Header().Set("WWW-Authenticate", "Basic realm=veraison")
106+
ReportProblem(o.logger, w, "wrong username or password")
107+
return
108+
}
109+
110+
log.Debugw("user authenticated", "user", userName)
111+
next.ServeHTTP(w, r)
112+
})
113+
}

Diff for: auth/iauthorizer.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2025 Contributors to the Veraison project.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package auth
5+
6+
import (
7+
"net/http"
8+
9+
"github.com/spf13/viper"
10+
"go.uber.org/zap"
11+
)
12+
13+
// IAuthorizer defines the interface that must be implemented by the veraison
14+
// auth backends.
15+
type IAuthorizer interface {
16+
// Init initializes the backend based on the configuration inside the
17+
// provided Viper object and using the provided logger.
18+
Init(v *viper.Viper, logger *zap.SugaredLogger) error
19+
20+
// Close terminates the backend. The exact nature of this method is
21+
// backend-specific.
22+
Close() error
23+
24+
// GetMiddleware returns an http.Handler that performs authorization
25+
GetMiddleware(http.Handler) http.Handler
26+
}

Diff for: auth/passthrough.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2025 Contributors to the Veraison project.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package auth
4+
5+
import (
6+
"errors"
7+
"net/http"
8+
9+
"github.com/spf13/viper"
10+
"go.uber.org/zap"
11+
)
12+
13+
type PassthroughAuthorizer struct {
14+
logger *zap.SugaredLogger
15+
}
16+
17+
func NewPassthroughAuthorizer(logger *zap.SugaredLogger) IAuthorizer {
18+
return &PassthroughAuthorizer{logger: logger}
19+
}
20+
21+
func (o *PassthroughAuthorizer) Init(v *viper.Viper, logger *zap.SugaredLogger) error {
22+
if logger == nil {
23+
return errors.New("nil logger")
24+
}
25+
o.logger = logger
26+
return nil
27+
}
28+
29+
func (o *PassthroughAuthorizer) Close() error {
30+
return nil
31+
}
32+
33+
func (o *PassthroughAuthorizer) GetMiddleware(next http.Handler) http.Handler {
34+
return http.HandlerFunc(
35+
func(w http.ResponseWriter, r *http.Request) {
36+
o.logger.Debugw("passthrough", "path", r.URL.Path)
37+
next.ServeHTTP(w, r)
38+
})
39+
}

Diff for: auth/problem.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2024 Contributors to the Veraison project.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package auth
4+
5+
import (
6+
"encoding/json"
7+
"net/http"
8+
9+
"github.com/moogar0880/problems"
10+
"github.com/veraison/ratsd/api"
11+
"go.uber.org/zap"
12+
)
13+
14+
func ReportProblem(logger *zap.SugaredLogger, w http.ResponseWriter, detail string) {
15+
p := &problems.DefaultProblem{
16+
Type: string(api.TagGithubCom2024VeraisonratsdErrorUnauthorized),
17+
Title: string(api.AccessUnauthorized),
18+
Detail: detail,
19+
Status: http.StatusUnauthorized,
20+
}
21+
22+
w.Header().Set("Content-Type", problems.ProblemMediaType)
23+
w.WriteHeader(p.ProblemStatus())
24+
json.NewEncoder(w).Encode(p)
25+
}

0 commit comments

Comments
 (0)