Skip to content

Commit

Permalink
fix: work in progree for issue #13
Browse files Browse the repository at this point in the history
implementing accesscontroller interface in order to be able to accept differnt oauth methods

Signed-off-by: guacamole <[email protected]>
  • Loading branch information
guacamole committed Oct 3, 2021
1 parent 3cfa2a9 commit efaadb9
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 26 deletions.
142 changes: 117 additions & 25 deletions auth/accesscontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import (
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
registry_auth "github.com/distribution/distribution/registry/auth"
"github.com/labstack/echo/v4"
"github.com/golang-jwt/jwt"
"io/ioutil"
"net/http"
"os"
"strings"

registry_auth "github.com/distribution/distribution/registry/auth"
"github.com/docker/libtrust"
"github.com/labstack/echo/v4"
)

//accessSet maps the named resource to a set of actions authorised
Expand All @@ -25,10 +28,10 @@ func newAccessSet(accessItems ...registry_auth.Access) accessSet {

for _, access := range accessItems {
resource := registry_auth.Resource{
Type: access.Type,
Name: access.Name,
Type: access.Type,
Name: access.Name,
}
set,exists := accessSet[resource]
set, exists := accessSet[resource]
if !exists {
set = newActionSet()
accessSet[resource] = set
Expand Down Expand Up @@ -60,22 +63,26 @@ type authChallenge struct {
accessSet accessSet
}

func (ac authChallenge) Error() string {
return ac.err.Error()
}

func (ac authChallenge) challengeParams(r *http.Request) string {
var realm string
if ac.autoRedirect{
if ac.autoRedirect {
realm = fmt.Sprintf("https://%s/auth/token", r.Host)
}else{
} else {
realm = ac.realm
}
str := fmt.Sprintf("Bearer realm=%q,service=%q", realm, ac.service)
if scope := ac.accessSet.scopeParam(); scope != "" {
str = fmt.Sprintf("%s,scope=%q", str, scope)
}

if ac.err.Error()== "ErrInvalidToken" || ac.err.Error() == "ErrMalformedToken" {
if ac.err.Error() == "ErrInvalidToken" || ac.err.Error() == "ErrMalformedToken" {
str = fmt.Sprintf("%s,error=%q", str, "invalid token")
} else if ac.err.Error() == "ErrInsufficientScope" {
str = fmt.Sprintf("%s,error=%q", str, "insufficient_scope")
str = fmt.Sprintf("%s,error=%q", str, "insufficient_scope")
}

return str
Expand All @@ -86,14 +93,13 @@ func (ac authChallenge) SetHeaders(r *http.Request, w http.ResponseWriter) {
w.Header().Add("WWW-Authenticate", ac.challengeParams(r))
}


//tokenAccessOptions is a convenience type for handling
//options to constructor of an accessController
type tokenAccessOptions struct {
realm string
autoRedirect bool
issuer string
service string
realm string
autoRedirect bool
issuer string
service string
rootCertBundle string
}

Expand All @@ -103,7 +109,7 @@ func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
keys := []string{"realm", "issuer", "service", "rootcertbundle"}
vals := make([]string, 0, len(keys))

for _,key := range keys {
for _, key := range keys {
val, ok := options[key].(string)
if !ok {
return opts, fmt.Errorf("token auth requires a valid option string: %q", key)
Expand Down Expand Up @@ -156,7 +162,7 @@ func newAccessController(options map[string]interface{}) (registry_auth.AccessCo
if len(rootCerts) == 0 {
return nil, fmt.Errorf("token auth requires atleast one token signing root certificate")
}

rootPool := x509.NewCertPool()
trustedKeys := make(map[string]libtrust.PublicKey, len(rootCerts))
for _, rootCert := range rootCerts {
Expand All @@ -167,34 +173,120 @@ func newAccessController(options map[string]interface{}) (registry_auth.AccessCo
}
trustedKeys[pubKey.KeyID()] = pubKey
}

return &auth{
store: nil,
c: nil,
store: nil,
c: nil,
realm: "",
autoRedirect: false,
issuer: "",
service: "",
rootCerts: nil,
trustedKeys: nil,
}, nil
}

var (
ErrNoRequestContext = errors.New("no http request in context")
ErrNoResponseWriterContext = errors.New("no http response in context")
ErrTokenRequired = errors.New("authorization token required")
ErrInsufficientScope = errors.New("insufficient scope")
ErrInvalidToken = errors.New("invalid token")
)

// VerifyOptions is used to specify
// options when verifying a JSON Web Token.
type VerifyOptions struct {
TrustedIssuers []string
AcceptedAudiences []string
Roots *x509.CertPool
TrustedKeys map[string]libtrust.PublicKey
}

// Authorized handles checking whether the given request is authorized
// for actions on resources described by the given access items.
func (a *auth) Authorized(ctx context.Context, access ...registry_auth.Access) (context.Context, error) {
if a.realm == "local" {
return nil, nil
func (a *auth) Authorized(ctx context.Context, accessItems ...registry_auth.Access) (context.Context, error) {
challenge := a.makeAuthChallenge(accessItems...)

req, ok := ctx.Value("http.request").(*http.Request)
if !ok {
return nil, ErrNoRequestContext
}

parts := strings.Split(req.Header.Get("Authorization"), " ")
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
challenge.err = ErrTokenRequired
return nil, challenge
}

token, err := a.verifyToken(parts[2])
if err != nil {
return nil, err
}

if a.realm == "akash" {
claims, ok := token.Claims.(*Claims)
if !ok {
return nil, ErrInvalidToken
}

accessSet := claims.accessSet()
for _, access := range accessItems {
if actionSet, ok := accessSet[access.Resource]; !ok {
if !actionSet.contains(access.Action) {
challenge.err = ErrInsufficientScope
return nil, challenge
}
}
}

return nil,nil

return nil, nil
}

func (a *auth) AuthorizedEcho() echo.MiddlewareFunc {
return func(handlerFunc echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
a.Authorized(c.Request().Context(), )
a.Authorized(c.Request().Context())
return nil
}
}
}

func (a *auth) makeAuthChallenge(access ...registry_auth.Access) authChallenge {
return authChallenge{
realm: a.realm,
autoRedirect: a.autoRedirect,
service: a.service,
accessSet: newAccessSet(access...),
}
}

func (a *auth) verifyToken(rawToken string) (*jwt.Token, error) {
token, err := jwt.Parse(rawToken, func(token *jwt.Token) (interface{}, error) {
if !token.Valid {
return nil, errors.New("JWT is invalid")
}
return token, nil
})
if err != nil {
return nil, err
}

verifyOpts := VerifyOptions{
TrustedIssuers: []string{a.issuer},
AcceptedAudiences: []string{a.service},
Roots: a.rootCerts,
TrustedKeys: a.trustedKeys,
}

claims, ok := token.Claims.(*Claims)
if !ok {
return nil, ErrInvalidToken
}

if err := claims.Verify(verifyOpts); err != nil {
return nil, err
}

return token, nil
}
47 changes: 47 additions & 0 deletions auth/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package auth

import (
"fmt"
registry_auth "github.com/distribution/distribution/registry/auth"
"github.com/golang-jwt/jwt"
"time"
)
Expand All @@ -11,6 +12,52 @@ type Claims struct {
Access AccessList
}

func (c *Claims) accessSet() accessSet {
if c == nil {
return nil
}

set := make(accessSet, len(c.Access))
for _, action := range c.Access {
r := registry_auth.Resource{
Type: action.Type,
Name: action.Name,
}

rr, ok := set[r]
if !ok {
rr := newActionSet()
set[r] = rr
}

for _, a := range action.Actions {
rr.add(a)
}
}

return set
}

func (c *Claims) Verify(opts VerifyOptions) error {

for _,aud := range opts.AcceptedAudiences {
if !c.VerifyAudience(aud, true){
return ErrInvalidToken
}
}
now := time.Now()
tExpiresAt := time.Unix(c.ExpiresAt, 0).Add(time.Minute)
if tExpiresAt.After(now){
return ErrInvalidToken
}
tNotBefore := time.Unix(c.NotBefore, 0).Add(time.Minute)
if tNotBefore.Before(now) {
return ErrInvalidToken
}

return nil
}

func (a *auth) newToken(u User, tokenLife int64) (string, error) {
//for now we're sending same name for sub and name.
//TODO when repositories need collaborators
Expand Down
1 change: 0 additions & 1 deletion cache/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ func New(storeLocation string) (Store, error) {
if err != nil {
return nil, err
}

return &dataStore{db: db}, nil
}

Expand Down
17 changes: 17 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
"fmt"
"os"
"strings"

"github.com/spf13/viper"
)
Expand All @@ -16,6 +17,7 @@ type (
SkynetPortalURL string `mapstructure:"skynet_portal_url"`
SigningSecret string `mapstructure:"signing_secret"`
SkynetConfig SkynetConfig `mapstructure:"skynet_config"`
BackendTypes []string `mapstructure:"backend_types"`
}

SkynetConfig struct {
Expand Down Expand Up @@ -52,6 +54,7 @@ func LoadFromENV() (*RegistryConfig, error) {
SkynetPortalURL: viper.GetString("SKYNET_PORTAL_URL"),
SigningSecret: viper.GetString("SIGNING_SECRET"),
SkynetConfig: SkynetConfig{},
BackendTypes: defaultAuthBackends(),
}

if config.SigningSecret == "" {
Expand All @@ -61,3 +64,17 @@ func LoadFromENV() (*RegistryConfig, error) {

return &config, nil
}

func defaultAuthBackends() []string {
backendList := viper.GetString("AUTH_BACKENDS")
if backendList != "" {
return strings.Split(backendList, ",")
}

return []string{
"github",
"http_basic_auth",
"token",
"silly",
}
}

0 comments on commit efaadb9

Please sign in to comment.