Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement auth #186

Merged
merged 3 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*.dll
*.so
*.dylib
*.bkp
policy/cmd/polcli/polcli
protogen
provisioning/cmd/provisioning-service/provisioning-service
Expand Down
133 changes: 133 additions & 0 deletions auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
## Overview

This directory implements authentication and authorization for Veraison API.
Authentication can be performed using the Basic HTTP scheme (with the `basic`
backend), or using a Bearer token (with the `keycloak` backend). Once an API
user is authenticated, authorization is
[role-based](https://en.wikipedia.org/wiki/Role-based_access_control). See
documentation for specific services for which role(s) are needed to access
their API.


## Configuration

- `backend`: specifies which auth backend will be used by the service. The
valid options are:

- `passthrough`: a backend that does not perform any authentication, allowing
all requests.
- `none`: alias for `passthrough`.
- `basic`: Uses the Basic HTTP authentication scheme. See
[RFC7617](https://datatracker.ietf.org/doc/html/rfc7617) for details. This
is not intended for production.
- `keycloak`: Uses OpenID Connect protocol as implemented by the Keycloak
authentication server.

See below for details of how to configure individual backends.

### Passthrough

No additional configuration is required. `passthrough` will allow all requests.
This is the default if `auth` configuration is not provided.

### Basic

- `users`: this is a mapping of user names onto their bcrypt password hashes
and roles. The key of the mapping is the user name, the value is a further
mapping for the details with the following fields:

- `password`: the bcrypt hash of the user's password.
- `roles`: either a single role or a list of roles associated with the
user. API authrization will be performed based on the user's roles.

On Linux, bcrypt hashes can be generated on the command line using `mkpasswd`
utility, e.g.:

```bash
mkpasswd -m bcrypt --stdin <<< Passw0rd!
```

For example:

```yaml
auth:
backend: basic
users:
user1:
password: "$2b$05$XgVBveh6QPrRHXI.8S/J9uobBR7Wv9z4CL8yACHEmKIQmYSSyKAqC" # Passw0rd!
roles: provisioner
user2:
password: "$2b$05$x5fvAV5WPkX0KXzqf5FMKODz0uyi2ioew1lOrF2Czp2aNH1LQmhki" # @s3cr3t
roles: [manager, provisioner]
```

### Keycloak

- `host` (optional): host name of the Keycloak service. Defaults to
`localhost`.
- `port` (optional): the port on which the Keycloak service is listening.
Defaults to `8080`.
- `realm` (optional): the Keycloak realm used by Veraison. A realm contains the
configuration for clients, users, roles, etc. It is roughly analogous to a
"tenant id". Defaults to `veraison`.

For example:

```yaml
auth:
backend: keycloak
host: keycloak.example.com
port: 11111
realm: veraison
```

## Usage

```go
"github.com/gin-gonic/gin"
"github.com/veraison/services/auth"
"github.com/veraison/services/config"
"github.com/veraison/services/log"

func main() {
// Load authroizer config.
v, err := config.ReadRawConfig(*config.File, false)
if err != nil {
log.Fatalf("Could not read config: %v", err)
}
subs, err := config.GetSubs(v, "*auth")
if err != nil {
log.Fatalf("Could not parse config: %v", err)
}

// Create new authorizer based on the loaded config.
authorizer, err := auth.NewAuthorizer(subs["auth"], log.Named("auth"))
if err != nil {
log.Fatalf("could not init authorizer: %v", err)
}

// Ensure the authorizer is terminated properly on exit
defer func() {
err := authorizer.Close()
if err != nil {
log.Errorf("Could not close authorizer: %v", err)
}
}()

// Use the authorizer to set a middleware handler in the appropriate gin
// router, with an appropriate role.
router := gin.Default()
router.Use(authorizer.GetGinHandler(auth.ManagerRole))

// Set up route handling here
// ...
// ...

// Run the service.
if err := router.Run("0.0.0.0:80"); err != nil {
log.Errorf("Gin engine failed: %v", err)
}
}

```

47 changes: 47 additions & 0 deletions auth/authorizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2023 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0

package auth

import (
"fmt"

"github.com/spf13/viper"
"github.com/veraison/services/config"
"go.uber.org/zap"
)

type cfg struct {
Backend string `mapstructure:"backend,omitempty"`
BackendConfigs map[string]interface{} `mapstructure:",remain"`
}

func NewAuthorizer(v *viper.Viper, logger *zap.SugaredLogger) (IAuthorizer, error) {
cfg := cfg{
Backend: "passthrough",
}

loader := config.NewLoader(&cfg)
if err := loader.LoadFromViper(v); err != nil {
return nil, err
}

var a IAuthorizer

switch cfg.Backend {
case "none", "passthrough":
a = &PassthroughAuthorizer{}
case "basic":
a = &BasicAuthorizer{}
case "keycloak":
a = &KeycloakAuthorizer{}
default:
return nil, fmt.Errorf("backend %q is not supported", cfg.Backend)
}

if err := a.Init(v, logger); err != nil {
return nil, err
}

return a, nil
}
153 changes: 153 additions & 0 deletions auth/basic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright 2023 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0

package auth

import (
"errors"
"fmt"
"net/http"

"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"github.com/veraison/services/log"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)

type basicAuthUser struct {
Password string `mapstructure:"password"`
Roles []string `mapstructure:"roles"`
}

func newBasicAuthUser(m map[string]interface{}) (*basicAuthUser, error) {
var newUser basicAuthUser

passRaw, ok := m["password"]
if !ok {
return nil, errors.New("password not set")
}

switch t := passRaw.(type) {
case string:
newUser.Password = t
default:
return nil, fmt.Errorf("invalid password: expected string found %T", t)
}

rolesRaw, ok := m["roles"]
if ok {
switch t := rolesRaw.(type) {
case []string:
newUser.Roles = t
case string:
newUser.Roles = make([]string, 1)
newUser.Roles[0] = t
default:
return nil, fmt.Errorf(
"invalid roles: expected []string or string, found %T",
t,
)
}
} else {
newUser.Roles = make([]string, 0)
}

return &newUser, nil
}

type BasicAuthorizer struct {
logger *zap.SugaredLogger
users map[string]*basicAuthUser
}

func (o *BasicAuthorizer) Init(v *viper.Viper, logger *zap.SugaredLogger) error {
if logger == nil {
return errors.New("nil logger")
}
o.logger = logger

o.users = make(map[string]*basicAuthUser)
if rawUsers := v.GetStringMap("users"); rawUsers != nil {
for name, rawUser := range rawUsers {
switch t := rawUser.(type) {
case map[string]interface{}:
newUser, err := newBasicAuthUser(t)
if err != nil {
return fmt.Errorf("invalid user %q: %w", name, err)

}
o.logger.Debugw("registered user",
"user", name,
"password", newUser.Password,
"roles", newUser.Roles,
)
o.users[name] = newUser
default:
return fmt.Errorf(
"invalid user %q: expected map[string]interface{}, got %T",
name, t,
)
}
}
}

return nil
}

func (o *BasicAuthorizer) Close() error {
return nil
}

func (o *BasicAuthorizer) GetGinHandler(role string) gin.HandlerFunc {
return func(c *gin.Context) {
o.logger.Debugw("auth basic", "path", c.Request.URL.Path)

userName, password, hasAuth := c.Request.BasicAuth()
if !hasAuth {
c.Writer.Header().Set("WWW-Authenticate", "Basic realm=veraison")
ReportProblem(c, http.StatusUnauthorized,
"no Basic Authorizaiton given")
return
}

userInfo, ok := o.users[userName]
if !ok {
c.Writer.Header().Set("WWW-Authenticate", "Basic realm=veraison")
ReportProblem(c, http.StatusUnauthorized,
"no Basic Authorizaiton given")
return
}

if err := bcrypt.CompareHashAndPassword(
[]byte(userInfo.Password),
[]byte(password),
); err != nil {
o.logger.Debugf("password check failed: %v", err)
c.Writer.Header().Set("WWW-Authenticate", "Basic realm=veraison")
ReportProblem(c, http.StatusUnauthorized,
"wrong username or password")
return
}

gotRole := false
if role == NoRole {
gotRole = true
} else {
for _, userRole := range userInfo.Roles {
if userRole == role {
gotRole = true
break
}
}
}

if gotRole {
log.Debugw("user authenticated", "user", userName, "role", role)
} else {
c.Writer.Header().Set("WWW-Authenticate", "Basic realm=veraison")
ReportProblem(c, http.StatusUnauthorized,
"API unauthorized for user")
}
}
}
25 changes: 25 additions & 0 deletions auth/iauthorizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2023 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0

package auth

import (
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"go.uber.org/zap"
)

// IAuthorizer defines the interface that must be implemented by the veraison
// auth backends.
type IAuthorizer interface {
// Init initializes the backend based on the configuration inside the
// provided Viper object and using the provided logger.
Init(v *viper.Viper, logger *zap.SugaredLogger) error
// Close terminates the backend. The exact nature of this method is
// backend-specific.
Close() error
// GetGinHandler returns a gin.HandlerFunc that performs authorization
// based on the specified role. This function can be set as gin
// middleware by passing it to gin.Engine.Use().
GetGinHandler(role string) gin.HandlerFunc
}
Loading