Skip to content

Commit b017cb7

Browse files
committed
feat: agent registry auth [WIP]
Signed-off-by: Jakob Steiner <[email protected]>
1 parent 709dedd commit b017cb7

10 files changed

+218
-28
lines changed

internal/auth/authentication.go

+22-7
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import (
66
"github.com/getsentry/sentry-go"
77
"github.com/glasskube/distr/internal/authjwt"
88
"github.com/glasskube/distr/internal/authn"
9+
"github.com/glasskube/distr/internal/authn/agent"
910
"github.com/glasskube/distr/internal/authn/authinfo"
1011
"github.com/glasskube/distr/internal/authn/authkey"
12+
"github.com/glasskube/distr/internal/authn/basic"
1113
"github.com/glasskube/distr/internal/authn/jwt"
1214
"github.com/glasskube/distr/internal/authn/token"
1315
internalctx "github.com/glasskube/distr/internal/context"
@@ -44,14 +46,27 @@ var AgentAuthentication = authn.New(
4446
// ArtifactsAuthentication supports Basic auth login for OCI clients, where the password should be a PAT.
4547
// The given PAT is verified against the database, to make sure that the user still exists.
4648
var ArtifactsAuthentication = authn.New(
47-
authn.Chain4(
48-
token.NewExtractor(
49-
token.WithExtractorFuncs(token.FromBasicAuth()),
50-
token.WithErrorHeaders(map[string]string{"WWW-Authenticate": "Basic realm=\"Distr\""}),
49+
authn.Alternative(
50+
// Authenticate with Agent JWT
51+
authn.Chain3(
52+
token.NewExtractor(token.WithExtractorFuncs(token.FromHeader("Bearer"))),
53+
jwt.Authenticator(authjwt.JWTAuth),
54+
authinfo.AgentJWTAuthenticator(),
55+
),
56+
// Auhtenticate UserAccount with PAT via BasicAuth
57+
authn.Chain3(
58+
token.NewExtractor(
59+
token.WithExtractorFuncs(token.FromBasicAuth()),
60+
token.WithErrorHeaders(http.Header{"WWW-Authenticate": []string{"Basic realm=\"Distr\""}}),
61+
),
62+
authkey.Authenticator(),
63+
authinfo.AuthKeyAuthenticator(),
64+
),
65+
// Authenticate Agent with ID and secret via BasicAuth
66+
authn.Chain(
67+
basic.Authenticator(),
68+
agent.Authenticator(),
5169
),
52-
authkey.Authenticator(),
53-
authinfo.AuthKeyAuthenticator(),
54-
authinfo.DbAuthenticator(),
5570
),
5671
)
5772

internal/authn/agent/authenticator.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package agent
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
11+
"github.com/glasskube/distr/internal/authjwt"
12+
"github.com/glasskube/distr/internal/authn"
13+
"github.com/glasskube/distr/internal/authn/authinfo"
14+
"github.com/glasskube/distr/internal/authn/basic"
15+
"github.com/glasskube/distr/internal/db"
16+
"github.com/glasskube/distr/internal/security"
17+
"github.com/glasskube/distr/internal/types"
18+
"github.com/google/uuid"
19+
)
20+
21+
func Authenticator() authn.Authenticator[basic.Auth, authinfo.AuthInfo] {
22+
fn := func(ctx context.Context, basic basic.Auth) (out authinfo.AuthInfo, err error) {
23+
if targetID, err1 := uuid.Parse(basic.Username); err1 != nil {
24+
err = fmt.Errorf("%w: %w", authn.ErrBadAuthentication, err1)
25+
} else if target, err1 := db.GetDeploymentTarget(ctx, targetID, nil); err1 != nil {
26+
err = err1
27+
} else if target.AccessKeyHash == nil || target.AccessKeySalt == nil {
28+
err = errors.New("access key or salt is nil")
29+
} else if err1 :=
30+
security.VerifyAccessKey(*target.AccessKeySalt, *target.AccessKeyHash, basic.Password); err1 != nil {
31+
err = fmt.Errorf("%w: %w", authn.ErrBadAuthentication, err1)
32+
} else if user, err1 := db.GetUserAccountByID(ctx, target.CreatedByUserAccountID); err1 != nil {
33+
err = err1
34+
} else if orgs, err1 := db.GetOrganizationsForUser(ctx, user.ID); err1 != nil {
35+
err = err1
36+
} else if len(orgs) != 1 {
37+
err = fmt.Errorf("user must have exactly one organization")
38+
} else if orgs[0].UserRole != types.UserRoleCustomer {
39+
err = fmt.Errorf("user must have role customer")
40+
} else if _, token, err1 := authjwt.GenerateDefaultToken(*user, orgs[0]); err1 != nil {
41+
err = err1
42+
} else {
43+
err = &tokenError{Token: token}
44+
}
45+
return
46+
}
47+
return authn.AuthenticatorFunc[basic.Auth, authinfo.AuthInfo](fn)
48+
}
49+
50+
type tokenError struct {
51+
Token string `json:"token"`
52+
}
53+
54+
var _ authn.WithResponseHeaders = &tokenError{}
55+
var _ authn.WithResponseStatus = &tokenError{}
56+
var _ authn.ResponseBodyWriter = &tokenError{}
57+
58+
// Error implements error.
59+
func (t *tokenError) Error() string {
60+
return "NOT AN ERROR: agent successful token auth" // :^)
61+
}
62+
63+
// ResponseHeaders implements authn.WithResponseHeaders.
64+
func (t *tokenError) ResponseHeaders() http.Header {
65+
result := http.Header{}
66+
result.Add("Content-Type", "application/json")
67+
return result
68+
}
69+
70+
// ResponseStatus implements authn.WithResponseStatus.
71+
func (t *tokenError) ResponseStatus() int {
72+
return http.StatusOK
73+
}
74+
75+
// WriteResponse implements authn.ResponseBodyWriter.
76+
func (t *tokenError) WriteResponse(w io.Writer) {
77+
_ = json.NewEncoder(w).Encode(t)
78+
}

internal/authn/authentication.go

+28-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"errors"
66
"net/http"
7+
8+
"go.uber.org/multierr"
79
)
810

911
type contextKey struct{}
@@ -76,16 +78,34 @@ func (a *Authentication[T]) ValidatorMiddleware(fn func(value T) error) func(nex
7678
}
7779

7880
func (a *Authentication[T]) handleError(w http.ResponseWriter, r *http.Request, err error) {
79-
hhe := &HttpHeaderError{}
80-
if errors.As(err, &hhe) {
81-
hhe.WriteTo(w)
81+
for _, err := range multierr.Errors(err) {
82+
var rh WithResponseHeaders
83+
if errors.As(err, &rh) {
84+
for key, value := range rh.ResponseHeaders() {
85+
for _, v := range value {
86+
w.Header().Add(key, v)
87+
}
88+
}
89+
}
8290
}
8391

84-
if errors.Is(err, ErrBadAuthentication) || errors.Is(err, ErrNoAuthentication) {
85-
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
86-
} else if a.unknownErrorHandler == nil {
87-
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
88-
} else {
92+
statusCode := http.StatusInternalServerError
93+
94+
var rs WithResponseStatus
95+
if errors.As(err, &rs) {
96+
statusCode = rs.ResponseStatus()
97+
} else if errors.Is(err, ErrBadAuthentication) || errors.Is(err, ErrNoAuthentication) {
98+
statusCode = http.StatusUnauthorized
99+
} else if a.unknownErrorHandler != nil {
89100
a.unknownErrorHandler(w, r, err)
101+
return
102+
}
103+
104+
var rw ResponseBodyWriter
105+
if errors.As(err, &rw) {
106+
w.WriteHeader(statusCode)
107+
rw.WriteResponse(w)
108+
} else {
109+
http.Error(w, http.StatusText(statusCode), statusCode)
90110
}
91111
}

internal/authn/authenticator.go

+23
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package authn
22

33
import (
44
"context"
5+
"errors"
56
"net/http"
7+
8+
"go.uber.org/multierr"
69
)
710

811
type Authenticator[IN any, OUT any] interface {
@@ -61,3 +64,23 @@ func Chain4[IN any, MID1 any, MID2 any, MID3 any, OUT any](
6164
}
6265
})
6366
}
67+
68+
func Alternative[A any, B any](authenticators ...Authenticator[A, B]) Authenticator[A, B] {
69+
return AuthenticatorFunc[A, B](func(ctx context.Context, in A) (result B, err error) {
70+
for _, authenticator := range authenticators {
71+
if out, err1 := authenticator.Authenticate(ctx, in); err1 != nil {
72+
multierr.AppendInto(&err, err1)
73+
if errors.Is(err, ErrNoAuthentication) || errors.Is(err, ErrBadAuthentication) {
74+
continue
75+
} else {
76+
break
77+
}
78+
} else {
79+
result = out
80+
err = nil
81+
break
82+
}
83+
}
84+
return
85+
})
86+
}

internal/authn/basic/authenticator.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package basic
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"github.com/glasskube/distr/internal/authn"
8+
)
9+
10+
type Auth struct {
11+
Username, Password string
12+
}
13+
14+
func Authenticator() authn.RequestAuthenticator[Auth] {
15+
fn := func(ctx context.Context, r *http.Request) (result Auth, err error) {
16+
if username, password, ok := r.BasicAuth(); !ok {
17+
err = authn.NewHttpHeaderError(
18+
authn.ErrNoAuthentication,
19+
http.Header{
20+
"WWW-Authenticate": []string{`Bearer realm="http://localhost:8585/v2/",service="localhost:8585"`, `Basic realm="Distr"`},
21+
},
22+
)
23+
} else {
24+
result = Auth{Username: username, Password: password}
25+
}
26+
return
27+
}
28+
return authn.AuthenticatorFunc[*http.Request, Auth](fn)
29+
}
30+
31+
func Password() authn.Authenticator[Auth, string] {
32+
return authn.AuthenticatorFunc[Auth, string](func(ctx context.Context, basic Auth) (string, error) {
33+
return basic.Password, nil
34+
})
35+
}

internal/authn/errors.go

+9-8
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,17 @@ var ErrBadAuthentication = errors.New("bad authentication")
1515

1616
type HttpHeaderError struct {
1717
wrapped error
18-
headers map[string]string
18+
headers http.Header
1919
}
2020

21-
func NewHttpHeaderError(err error, headers map[string]string) error {
21+
// ResponseHEaders implements WithResponseHeaders.
22+
func (err *HttpHeaderError) ResponseHeaders() http.Header {
23+
return err.headers
24+
}
25+
26+
var _ WithResponseHeaders = &HttpHeaderError{}
27+
28+
func NewHttpHeaderError(err error, headers http.Header) error {
2229
return &HttpHeaderError{
2330
wrapped: err,
2431
headers: headers,
@@ -32,9 +39,3 @@ func (err *HttpHeaderError) Error() string {
3239
func (err *HttpHeaderError) Unwrap() error {
3340
return err.wrapped
3441
}
35-
36-
func (err *HttpHeaderError) WriteTo(w http.ResponseWriter) {
37-
for k, v := range err.headers {
38-
w.Header().Set(k, v)
39-
}
40-
}

internal/authn/response.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package authn
2+
3+
import (
4+
"io"
5+
"net/http"
6+
)
7+
8+
type WithResponseHeaders interface {
9+
ResponseHeaders() http.Header
10+
}
11+
12+
type WithResponseStatus interface {
13+
ResponseStatus() int
14+
}
15+
16+
type ResponseBodyWriter interface {
17+
WriteResponse(w io.Writer)
18+
}

internal/authn/token/token.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ type TokenExtractorFunc func(r *http.Request) string
1212

1313
type TokenExtractor struct {
1414
fns []TokenExtractorFunc
15-
headers map[string]string
15+
headers http.Header
1616
}
1717

1818
// Authenticate implements Provider.
@@ -35,7 +35,7 @@ func WithExtractorFuncs(fns ...TokenExtractorFunc) ExtractorOption {
3535
}
3636
}
3737

38-
func WithErrorHeaders(headers map[string]string) ExtractorOption {
38+
func WithErrorHeaders(headers http.Header) ExtractorOption {
3939
return func(te *TokenExtractor) {
4040
te.headers = headers
4141
}

internal/db/organization.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func UpdateOrganization(ctx context.Context, org *types.Organization) error {
5858
}
5959
}
6060

61-
func GetOrganizationsForUser(ctx context.Context, userID uuid.UUID) ([]*types.OrganizationWithUserRole, error) {
61+
func GetOrganizationsForUser(ctx context.Context, userID uuid.UUID) ([]types.OrganizationWithUserRole, error) {
6262
db := internalctx.GetDb(ctx)
6363
rows, err := db.Query(ctx, `
6464
SELECT`+organizationOutputExpr+`, j.user_role
@@ -70,7 +70,7 @@ func GetOrganizationsForUser(ctx context.Context, userID uuid.UUID) ([]*types.Or
7070
if err != nil {
7171
return nil, err
7272
}
73-
result, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[types.OrganizationWithUserRole])
73+
result, err := pgx.CollectRows(rows, pgx.RowToStructByName[types.OrganizationWithUserRole])
7474
if err != nil {
7575
return nil, err
7676
} else {

internal/handlers/auth.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func authLoginHandler(w http.ResponseWriter, r *http.Request) {
7171
}
7272
org := orgs[0]
7373

74-
if _, tokenString, err := authjwt.GenerateDefaultToken(*user, *org); err != nil {
74+
if _, tokenString, err := authjwt.GenerateDefaultToken(*user, org); err != nil {
7575
return fmt.Errorf("token creation failed: %w", err)
7676
} else if err = db.UpdateUserAccountLastLoggedIn(ctx, user.ID); err != nil {
7777
return err

0 commit comments

Comments
 (0)