Skip to content

Commit

Permalink
[CLOUDTRUST-2375] Add middleware to enable feature according to reaml…
Browse files Browse the repository at this point in the history
… conf.
  • Loading branch information
fperot74 committed Mar 3, 2020
1 parent b9f14ad commit 74a6646
Show file tree
Hide file tree
Showing 10 changed files with 299 additions and 16 deletions.
32 changes: 32 additions & 0 deletions configuration/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ package configuration

import "encoding/json"

// Constants
const (
CheckKeyIDNow = "IDNow"
CheckKeyPhysical = "physical-check"
)

var (
// AvailableCheckKeys lists all available check keys for RealmAdminConfiguration
AvailableCheckKeys = []string{CheckKeyIDNow, CheckKeyPhysical}
)

// RealmConfiguration struct. APISelfAccountEditingEnabled replaces former field APISelfMailEditingEnabled
type RealmConfiguration struct {
DefaultClientID *string `json:"default_client_id,omitempty"`
Expand All @@ -21,6 +32,20 @@ type RealmConfiguration struct {
RedirectSuccessfulRegistrationURL *string `json:"redirect_successful_registration_url,omitempty"`
}

// RealmAdminConfiguration struct
type RealmAdminConfiguration struct {
Mode *string `json:"mode"`
AvailableChecks map[string]bool `json:"available-checks,omitempty"`
Accreditations []RealmAdminAccreditation `json:"accreditations,omitempty"`
}

// RealmAdminAccreditation struct
type RealmAdminAccreditation struct {
Type *string `json:"type,omitempty"`
Validity *string `json:"validity,omitempty"`
Condition *string `json:"condition,omitempty"`
}

// Authorization struct
type Authorization struct {
RealmID *string `json:"realm_id"`
Expand All @@ -43,3 +68,10 @@ func NewRealmConfiguration(confJSON string) (RealmConfiguration, error) {
conf.DeprecatedAPISelfMailEditingEnabled = nil
return conf, nil
}

// NewRealmAdminConfiguration returns the realm admin configuration from its JSON representation
func NewRealmAdminConfiguration(configJSON string) (RealmAdminConfiguration, error) {
var conf RealmAdminConfiguration
var err = json.Unmarshal([]byte(configJSON), &conf)
return conf, err
}
11 changes: 11 additions & 0 deletions configuration/dto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ func TestNewRealmConfiguration(t *testing.T) {
assert.False(t, *conf.APISelfAccountEditingEnabled)
})
}

func TestNewRealmAdminConfiguration(t *testing.T) {
t.Run("Invalid JSON", func(t *testing.T) {
var _, err = NewRealmAdminConfiguration(`{`)
assert.NotNil(t, err)
})
t.Run("Valid JSON", func(t *testing.T) {
var _, err = NewRealmAdminConfiguration(`{}`)
assert.Nil(t, err)
})
}
3 changes: 3 additions & 0 deletions configuration/mock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package configuration

//go:generate mockgen -destination=./mock/cloudtrustdb.go -package=mock -mock_names=CloudtrustDB=CloudtrustDB,SQLRow=SQLRow github.com/cloudtrust/common-service/database/sqltypes CloudtrustDB,SQLRow
20 changes: 18 additions & 2 deletions configuration/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import (
)

const (
selectConfigStmt = `SELECT configuration FROM realm_configuration WHERE (realm_id = ?)`
selectAllAuthzStmt = `SELECT realm_id, group_name, action, target_realm_id, target_group_name FROM authorizations;`
selectConfigStmt = `SELECT configuration FROM realm_configuration WHERE realm_id = ? AND configuration IS NOT NULL`
selectAdminConfigStmt = `SELECT admin_configuration FROM realm_configuration WHERE realm_id = ? AND admin_configuration IS NOT NULL`
selectAllAuthzStmt = `SELECT realm_id, group_name, action, target_realm_id, target_group_name FROM authorizations;`
)

// ConfigurationReaderDBModule struct
Expand Down Expand Up @@ -46,6 +47,21 @@ func (c *ConfigurationReaderDBModule) GetConfiguration(ctx context.Context, real
}
}

// GetAdminConfiguration returns a realm admin configuration
func (c *ConfigurationReaderDBModule) GetAdminConfiguration(ctx context.Context, realmID string) (RealmAdminConfiguration, error) {
var configJSON string
row := c.db.QueryRow(selectAdminConfigStmt, realmID)

var err = row.Scan(&configJSON)
if err != nil {
if err == sql.ErrNoRows {
c.logger.Warn(ctx, "msg", "Realm Admin Configuration not found in DB", "error", err.Error())
}
return RealmAdminConfiguration{}, err
}
return NewRealmAdminConfiguration(configJSON)
}

// GetAuthorizations returns authorizations
func (c *ConfigurationReaderDBModule) GetAuthorizations(ctx context.Context) ([]Authorization, error) {
// Get Authorizations from DB
Expand Down
83 changes: 83 additions & 0 deletions configuration/module_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package configuration

import (
"context"
"database/sql"
"errors"
"testing"

"github.com/cloudtrust/common-service/configuration/mock"
"github.com/cloudtrust/common-service/log"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)

func TestGetConfiguration(t *testing.T) {
var mockCtrl = gomock.NewController(t)
defer mockCtrl.Finish()

var mockDB = mock.NewCloudtrustDB(mockCtrl)
var mockSQLRow = mock.NewSQLRow(mockCtrl)
var logger = log.NewNopLogger()

var realmID = "myrealm"
var ctx = context.TODO()
var module = NewConfigurationReaderDBModule(mockDB, logger)

t.Run("SQL error", func(t *testing.T) {
mockDB.EXPECT().QueryRow(gomock.Any(), realmID).Return(mockSQLRow)
mockSQLRow.EXPECT().Scan(gomock.Any()).Return(errors.New("SQL error"))
var _, err = module.GetConfiguration(ctx, realmID)
assert.NotNil(t, err)
})
t.Run("SQL No row", func(t *testing.T) {
mockDB.EXPECT().QueryRow(gomock.Any(), realmID).Return(mockSQLRow)
mockSQLRow.EXPECT().Scan(gomock.Any()).Return(sql.ErrNoRows)
var _, err = module.GetConfiguration(ctx, realmID)
assert.NotNil(t, err)
})
t.Run("Success", func(t *testing.T) {
mockDB.EXPECT().QueryRow(gomock.Any(), realmID).Return(mockSQLRow)
mockSQLRow.EXPECT().Scan(gomock.Any()).DoAndReturn(func(ptrConfig *string) error {
*ptrConfig = `{}`
return nil
})
var _, err = module.GetConfiguration(ctx, realmID)
assert.Nil(t, err)
})
}

func TestGetAdminConfiguration(t *testing.T) {
var mockCtrl = gomock.NewController(t)
defer mockCtrl.Finish()

var mockDB = mock.NewCloudtrustDB(mockCtrl)
var mockSQLRow = mock.NewSQLRow(mockCtrl)
var logger = log.NewNopLogger()

var realmID = "myrealm"
var ctx = context.TODO()
var module = NewConfigurationReaderDBModule(mockDB, logger)

t.Run("SQL error", func(t *testing.T) {
mockDB.EXPECT().QueryRow(gomock.Any(), realmID).Return(mockSQLRow)
mockSQLRow.EXPECT().Scan(gomock.Any()).Return(errors.New("SQL error"))
var _, err = module.GetAdminConfiguration(ctx, realmID)
assert.NotNil(t, err)
})
t.Run("SQL No row", func(t *testing.T) {
mockDB.EXPECT().QueryRow(gomock.Any(), realmID).Return(mockSQLRow)
mockSQLRow.EXPECT().Scan(gomock.Any()).Return(sql.ErrNoRows)
var _, err = module.GetAdminConfiguration(ctx, realmID)
assert.NotNil(t, err)
})
t.Run("Success", func(t *testing.T) {
mockDB.EXPECT().QueryRow(gomock.Any(), realmID).Return(mockSQLRow)
mockSQLRow.EXPECT().Scan(gomock.Any()).DoAndReturn(func(ptrConfig *string) error {
*ptrConfig = `{}`
return nil
})
var _, err = module.GetAdminConfiguration(ctx, realmID)
assert.Nil(t, err)
})
}
9 changes: 9 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const (
MsgErrInvalidPathParam = "invalidPathParameter"
MsgErrInvalidParam = "invalidParameter"
MsgErrOpNotPermitted = "operationNotPermitted"
MsgErrDisabledEndpoint = "disabledEndpoint"

AuthHeader = "authorizationHeader"
BasicToken = "basicToken"
Expand Down Expand Up @@ -98,3 +99,11 @@ func CreateNotFoundError(messageKey string) Error {
Message: GetEmitter() + "." + messageKey,
}
}

// CreateEndpointNotEnabled creates an error relative to an attempt to access a not enabled endpoint
func CreateEndpointNotEnabled(param string) Error {
return Error{
Status: http.StatusConflict,
Message: fmt.Sprintf("%s.%s.%s", GetEmitter(), MsgErrDisabledEndpoint, param),
}
}
2 changes: 0 additions & 2 deletions middleware/authentication_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package middleware

//go:generate mockgen -destination=./mock/keycloak_client.go -package=mock -mock_names=KeycloakClient=KeycloakClient github.com/cloudtrust/common-service/middleware KeycloakClient

import (
"bytes"
"context"
Expand Down
55 changes: 55 additions & 0 deletions middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package middleware

import (
"context"
"errors"
"net/http"
"time"

cs "github.com/cloudtrust/common-service"
"github.com/cloudtrust/common-service/configuration"
errorhandler "github.com/cloudtrust/common-service/errors"
"github.com/cloudtrust/common-service/log"
"github.com/cloudtrust/common-service/metrics"
Expand Down Expand Up @@ -37,6 +39,59 @@ func MakeEndpointInstrumentingMW(m metrics.Metrics, histoName string) cs.Middlew
}
}

// IDRetriever is an interface to get an ID using an object's name
type IDRetriever interface {
GetID(accessToken, name string) (string, error)
}

// AdminConfigurationRetriever is an interface to get an admin configuration
type AdminConfigurationRetriever interface {
GetAdminConfiguration(ctx context.Context, realmID string) (configuration.RealmAdminConfiguration, error)
}

// MakeEndpointAvailableCheckMW makes a middleware that ensure a feature is enabled at admin configuration level in the current context
func MakeEndpointAvailableCheckMW(enabledKey string, idRetriever IDRetriever, confRetriever AdminConfigurationRetriever, logger log.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
var ctx = req.Context()
var realmName = ctx.Value(cs.CtContextRealm).(string)
var accessToken = ctx.Value(cs.CtContextAccessToken).(string)
// Get realm ID
var realmID, err = idRetriever.GetID(accessToken, realmName)
if err != nil {
logger.Info(ctx, "msg", "Can't get realm ID", "realm", realmName)
handleError(req.Context(), err, w)
return
}
// Get admin configuration
var conf configuration.RealmAdminConfiguration
conf, err = confRetriever.GetAdminConfiguration(ctx, realmID)
if err != nil {
logger.Info(ctx, "msg", "Can't get realm admin configuration", "realm", realmName)
handleError(req.Context(), err, w)
return
}
if !conf.AvailableChecks[enabledKey] {
logger.Info(ctx, "msg", "Feature not enabled", "realm", realmName, "feat", enabledKey)
handleError(req.Context(), errorhandler.CreateEndpointNotEnabled(realmName), w)
return
}

ctx = context.WithValue(ctx, cs.CtContextRealmID, realmID)
next.ServeHTTP(w, req.WithContext(ctx))
})
}
}

func handleError(ctx context.Context, err error, w http.ResponseWriter) {
switch e := err.(type) {
case errorhandler.Error:
httpErrorHandler(ctx, e.Status, e, w)
default:
httpErrorHandler(ctx, http.StatusInternalServerError, errors.New("unexpected.error"), w)
}
}

func httpErrorHandler(_ context.Context, statusCode int, err error, w http.ResponseWriter) {
w.WriteHeader(statusCode)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
Expand Down
Loading

0 comments on commit 74a6646

Please sign in to comment.