-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement API authentication and authorization. - Define IAuthorizer interface that can be used to obtain a gin middleware handler that performs authorisation. - Add a mechanism to obtain an IAuthorizer for a particular role base on Veraison configuration. - Implement "passthrough" authorizer that duplicates existing behavior (no auth). - Implement "basic" authorizer that does not rely on an external authorization server. - Implement "keycloak" authorizer that uses Keycloak service for authentication. - Update provisioning service to authorize based on "provisioner" role. - Update management service to authorize based on "manager" role. - Add the previously missing README for the management service. Signed-off-by: Sergei Trofimov <[email protected]>
- Loading branch information
Showing
39 changed files
with
2,681 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 futher | ||
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. | ||
|
||
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) | ||
} | ||
} | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
// 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") | ||
c.AbortWithStatus(http.StatusUnauthorized) | ||
return | ||
} | ||
|
||
userInfo, ok := o.users[userName] | ||
if !ok { | ||
c.Writer.Header().Set("WWW-Authenticate", "Basic realm=veraison") | ||
c.AbortWithStatus(http.StatusUnauthorized) | ||
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") | ||
c.AbortWithStatus(http.StatusUnauthorized) | ||
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") | ||
c.AbortWithStatus(http.StatusUnauthorized) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.