From f53c23311e946ef5dbc1bc9438308bb966e9c7f4 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Wed, 3 Nov 2021 14:51:25 +0100 Subject: [PATCH 1/2] Refactor role security, support it for ext. auth flow --- auth/oauth2/user_authorizer.go | 14 +++++++-- pkg/auth/role-security.go | 41 +++++++++++++++++++++++++ pkg/auth/role-security_test.go | 56 ++++++++++++++++++++++++++++++++++ system/service/auth.go | 34 +++++++++++++++++++++ system/types/app_settings.go | 34 +++++++++++++++++++++ system/types/auth_client.go | 31 ------------------- 6 files changed, 176 insertions(+), 34 deletions(-) create mode 100644 pkg/auth/role-security.go create mode 100644 pkg/auth/role-security_test.go diff --git a/auth/oauth2/user_authorizer.go b/auth/oauth2/user_authorizer.go index 3cf5ba6be..e34b60bb6 100644 --- a/auth/oauth2/user_authorizer.go +++ b/auth/oauth2/user_authorizer.go @@ -2,9 +2,12 @@ package oauth2 import ( "fmt" - "github.com/cortezaproject/corteza-server/auth/request" - "github.com/go-oauth2/oauth2/v4/server" "net/http" + + "github.com/cortezaproject/corteza-server/auth/request" + internalAuth "github.com/cortezaproject/corteza-server/pkg/auth" + "github.com/cortezaproject/corteza-server/pkg/payload" + "github.com/go-oauth2/oauth2/v4/server" ) func NewUserAuthorizer(sm *request.SessionManager, loginURL, clientAuthURL string) server.UserAuthorizationHandler { @@ -40,7 +43,12 @@ func NewUserAuthorizer(sm *request.SessionManager, loginURL, clientAuthURL strin var roles = request.GetRoleMemberships(ses) if client.Security != nil { // filter user's roles with client security settings - roles = client.Security.ProcessRoles(roles...) + roles = internalAuth.ApplyRoleSecurity( + payload.ParseUint64s(client.Security.PermittedRoles), + payload.ParseUint64s(client.Security.ProhibitedRoles), + payload.ParseUint64s(client.Security.ForcedRoles), + roles..., + ) } // User authenticated, client authorized! diff --git a/pkg/auth/role-security.go b/pkg/auth/role-security.go new file mode 100644 index 000000000..78ecf0edc --- /dev/null +++ b/pkg/auth/role-security.go @@ -0,0 +1,41 @@ +package auth + +import ( + "sort" + + "github.com/cortezaproject/corteza-server/pkg/slice" +) + +// ApplyRoleSecurity takes role security params (set of permitted, prohibited and forced roles) +// and applies these rules to the set of given roles +// +// Filtered set of roles is returned +// +// String slices are used intentionally, because of the data source used +func ApplyRoleSecurity(permitted, prohibited, forced []uint64, rr ...uint64) (out []uint64) { + var ( + mPermitted = slice.ToUint64BoolMap(permitted) + mProhibited = slice.ToUint64BoolMap(prohibited) + mForced = slice.ToUint64BoolMap(forced) + ) + + // iterate over user's roles and just append them (obeying allow&deny rules) + // to list of mForced roles + for _, r := range rr { + if (len(mPermitted) == 0 || mPermitted[r]) && !mProhibited[r] { + mForced[r] = true + } + } + + out = make([]uint64, 0, len(mForced)) + for forcedRoleID := range mForced { + out = append(out, forcedRoleID) + } + + // for stable output + sort.Slice(out, func(i, j int) bool { + return out[i] < out[j] + }) + + return +} diff --git a/pkg/auth/role-security_test.go b/pkg/auth/role-security_test.go new file mode 100644 index 000000000..9cba948b9 --- /dev/null +++ b/pkg/auth/role-security_test.go @@ -0,0 +1,56 @@ +package auth + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestApplyRoleSecurity(t *testing.T) { + tests := []struct { + name string + permitted []uint64 + prohibited []uint64 + forced []uint64 + roles []uint64 + wantOut []uint64 + }{ + { + "empty", + []uint64{}, + []uint64{}, + []uint64{}, + []uint64{}, + []uint64{}, + }, + { + "nil", + nil, + nil, + nil, + nil, + []uint64{}, + }, + { + "one", + []uint64{1}, + []uint64{2}, + []uint64{3}, + []uint64{1, 2}, + []uint64{1, 3}, + }, + { + "forced only", + []uint64{}, + []uint64{}, + []uint64{3, 2, 1}, + []uint64{2}, + []uint64{1, 2, 3}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.wantOut, ApplyRoleSecurity(tt.permitted, tt.prohibited, tt.forced, tt.roles...)) + }) + } +} diff --git a/system/service/auth.go b/system/service/auth.go index 070f66176..5d04136d2 100644 --- a/system/service/auth.go +++ b/system/service/auth.go @@ -15,6 +15,7 @@ import ( "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/handle" + "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/cortezaproject/corteza-server/pkg/rand" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/system/service/event" @@ -962,6 +963,39 @@ func (svc auth) procLogin(ctx context.Context, s store.Storer, u *types.User, c return } + { + var eapSec *types.ExternalAuthProviderSecurity + + switch p.Provider { + case credentialsTypePassword: + // nothing to do with password provider + + case "saml": + // we need to fetch SAML security settings from different part of settings + eapSec = &CurrentSettings.Auth.External.Saml.Security + + default: + eap := CurrentSettings.Auth.External.Providers.FindByHandle(p.Provider) + + if eap != nil { + eapSec = &eap.Security + } + + } + + if eapSec != nil { + // if authenticated with external auth provider + // there might be additional roles that need to be + // set to this security session + u.SetRoles(internalAuth.ApplyRoleSecurity( + payload.ParseUint64s(eapSec.PermittedRoles), + payload.ParseUint64s(eapSec.ProhibitedRoles), + payload.ParseUint64s(eapSec.ForcedRoles), + u.Roles()..., + )...) + } + } + if c != nil { c.LastUsedAt = now() if err = store.UpdateCredentials(ctx, s, c); err != nil { diff --git a/system/types/app_settings.go b/system/types/app_settings.go index e0c74d06b..74b4ae9e6 100644 --- a/system/types/app_settings.go +++ b/system/types/app_settings.go @@ -110,6 +110,8 @@ type ( IdentHandle string `kv:"ident-handle"` IdentIdentifier string `kv:"ident-identifier"` } `kv:"idp"` + + Security ExternalAuthProviderSecurity `json:"-" kv:"security,final"` } // all external providers we know @@ -223,6 +225,38 @@ type ( RedirectUrl string `json:"-" kv:"redirect"` IssuerUrl string `json:"-" kv:"issuer"` Weight int `json:"-"` + + Security ExternalAuthProviderSecurity `json:"-" kv:"security,final"` + } + + ExternalAuthProviderSecurity struct { + // Subset of roles, permitted to be used with this client + // when authorizing via this auth provider. + // + // IDs are intentionally stored as strings to support JS (int64 only) + // + PermittedRoles []string `json:"permittedRoles,omitempty"` + + // Subset of roles, prohibited to be used with this client + // when authorizing via this auth provider. + // + // IDs are intentionally stored as strings to support JS (int64 only) + // + ProhibitedRoles []string `json:"prohibitedRoles,omitempty"` + + // Set of additional roles that are forced on this user + // when authorizing via this auth provider. + // + // IDs are intentionally stored as strings to support JS (int64 only) + ForcedRoles []string `json:"forcedRoles,omitempty"` + + // Map external roles or groups to internal + // + // If IdP provides a list of roles (groups) along side authenticated user + // these roles can be mapped to the valid local roles + // + // @todo implement mapped roles + // MappedRoles map[string]string `json:"mappedRoles,omitempty"` } SmtpServers struct { diff --git a/system/types/auth_client.go b/system/types/auth_client.go index 970eca855..f1be51e02 100644 --- a/system/types/auth_client.go +++ b/system/types/auth_client.go @@ -4,11 +4,9 @@ import ( "database/sql/driver" "encoding/json" "fmt" - "strconv" "time" "github.com/cortezaproject/corteza-server/pkg/filter" - "github.com/cortezaproject/corteza-server/pkg/slice" ) type ( @@ -221,32 +219,3 @@ func (vv *AuthClientSecurity) Value() (driver.Value, error) { return json.Marshal(vv) } - -// Takes user's roles, filter out only allowed roles (when set), remove denied and add all forced -func (vv *AuthClientSecurity) ProcessRoles(rr ...uint64) (out []uint64) { - var ( - permitted = slice.ToStringBoolMap(vv.PermittedRoles) - prohibited = slice.ToStringBoolMap(vv.ProhibitedRoles) - forced = slice.ToStringBoolMap(vv.ForcedRoles) - aux string - roleID uint64 - ) - - // iterate over user's roles and just append them (obeying allow&deny rules) - // to list of forced roles - for _, r := range rr { - aux = strconv.FormatUint(r, 10) - if (len(vv.PermittedRoles) == 0 || permitted[aux]) && !prohibited[aux] { - forced[aux] = true - } - } - - out = make([]uint64, 0, len(forced)) - for i := range forced { - if roleID, _ = strconv.ParseUint(i, 10, 64); roleID > 0 { - out = append(out, roleID) - } - } - - return -} From 0114411f180f1d56b8e062b80bc2d9f92f3432fe Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Tue, 23 Nov 2021 13:43:22 +0100 Subject: [PATCH 2/2] Allow settings to be removed (via rest-bulkset) --- system/service/settings.go | 4 ++++ system/types/settings.go | 21 ++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/system/service/settings.go b/system/service/settings.go index ccf4e1877..0b0a20146 100644 --- a/system/service/settings.go +++ b/system/service/settings.go @@ -232,6 +232,10 @@ func (svc *settings) BulkSet(ctx context.Context, vv types.SettingValueSet) (err return } + if err = store.DeleteSetting(ctx, svc.store, vv.Trash()...); err != nil { + return + } + svc.logChange(ctx, vv) return svc.updateCurrent(ctx, vv) diff --git a/system/types/settings.go b/system/types/settings.go index 1ef0ceece..d4f57c821 100644 --- a/system/types/settings.go +++ b/system/types/settings.go @@ -88,6 +88,10 @@ func (v *SettingValue) Bool() (out bool) { return } +func (v *SettingValue) IsNull() (out bool) { + return v == nil || string(v.Value) == "null" || string(v.Value) == "" +} + func (v *SettingValue) NormalizeValue() { } @@ -232,7 +236,7 @@ func (set SettingValueSet) New(in SettingValueSet) (out SettingValueSet) { org := set.KV() for _, v := range in { - if !org.Has(v.Name) { + if !org.Has(v.Name) && !v.IsNull() { out = append(out, v) } } @@ -240,12 +244,23 @@ func (set SettingValueSet) New(in SettingValueSet) (out SettingValueSet) { return } -// New returns all new values (that do not exist in the original set) +// New returns all old values (that exist in the original set) func (set SettingValueSet) Old(in SettingValueSet) (out SettingValueSet) { org := set.KV() for _, v := range in { - if org.Has(v.Name) { + if org.Has(v.Name) && !v.IsNull() { + out = append(out, v) + } + } + + return +} + +// Trash returns values from the set that were set ti nil +func (set SettingValueSet) Trash() (out SettingValueSet) { + for _, v := range set { + if v.IsNull() { out = append(out, v) } }