snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1,169 @@
package request
import (
"encoding/gob"
"github.com/cortezaproject/corteza/server/auth/settings"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/gorilla/sessions"
)
type (
// handles authentication state, keeps info about permanent login and
// status of the MFA
authUser struct {
User *types.User
// is user logged-in with "remember-me"?
PermSession bool
MFAStatus map[authType]authStatus
}
authStatus uint
authType string
)
const (
// not enforced by user or by global settings
authStatusDisabled authStatus = iota
// when mfa is unconfigured on user but enforced globally
// this can happen on when new user registers or when global enforcement is later enabled
authStatusUnconfigured
// enforced by user or global settings, but not yet verified
authStatusPending
// verified
authStatusOK
)
const (
// password does not really have any function,
// more for demonstration purposes
authByPassword = "password"
authByEmailOTP = "email-otp"
authByTOTP = "totp"
)
func init() {
gob.Register(&authUser{})
}
func NewAuthUser(s *settings.Settings, u *types.User, perm bool) *authUser {
au := &authUser{
User: u,
PermSession: perm,
MFAStatus: make(map[authType]authStatus),
}
au.set(s, u)
return au
}
func (au *authUser) Update(s *settings.Settings, u *types.User) {
au.set(s, u)
}
func (au *authUser) set(s *settings.Settings, u *types.User) {
if u == nil || s == nil {
return
}
if u.Meta == nil {
u.Meta = &types.UserMeta{}
}
// User's MFA security policy
umsp := u.Meta.SecurityPolicy.MFA
// Global MFA security policy
gmsp := s.MultiFactor
mfaStatus := map[authType]authStatus{
authByPassword: authStatusOK,
authByEmailOTP: authStatusDisabled,
authByTOTP: authStatusDisabled,
}
// determinate mfa status for email OTP
if !gmsp.EmailOTP.Enabled {
mfaStatus[authByEmailOTP] = authStatusDisabled
} else if umsp.EnforcedEmailOTP || gmsp.EmailOTP.Enforced {
mfaStatus[authByEmailOTP] = authStatusPending
}
// determinate mfa status for TOTP
if !gmsp.TOTP.Enabled {
mfaStatus[authByTOTP] = authStatusDisabled
} else if !umsp.EnforcedTOTP && gmsp.TOTP.Enforced {
// TOTP not enforced on user but enforced globally
mfaStatus[authByTOTP] = authStatusUnconfigured
} else if umsp.EnforcedTOTP || gmsp.TOTP.Enforced {
mfaStatus[authByTOTP] = authStatusPending
}
au.MFAStatus = mfaStatus
}
func (au authUser) DisabledEmailOTP() bool {
return au.MFAStatus[authByEmailOTP] == authStatusDisabled
}
func (au authUser) PendingEmailOTP() bool {
return au.MFAStatus[authByEmailOTP] == authStatusPending
}
func (au authUser) DisabledTOTP() bool {
return au.MFAStatus[authByTOTP] == authStatusDisabled
}
func (au authUser) UnconfiguredTOTP() bool {
return au.MFAStatus[authByTOTP] == authStatusUnconfigured
}
func (au authUser) PendingTOTP() bool {
return au.MFAStatus[authByTOTP] == authStatusPending
}
// PendingMFA Returns true if any of MFAs are pending
func (au authUser) PendingMFA() bool {
for _, st := range au.MFAStatus {
if st == authStatusPending {
return true
}
}
return false
}
func (au *authUser) CompleteEmailOTP() {
au.MFAStatus[authByEmailOTP] = authStatusOK
}
func (au *authUser) CompleteTOTP() {
au.MFAStatus[authByTOTP] = authStatusOK
}
func (au *authUser) ResetTOTP() {
au.MFAStatus[authByTOTP] = authStatusUnconfigured
}
func (au *authUser) Forget(ses *sessions.Session) {
delete(ses.Values, keyAuthUser)
delete(ses.Values, keyRememberMe)
}
func (au *authUser) Save(ses *sessions.Session) {
ses.Values[keyAuthUser] = au
// explicitly save roles
ses.Values[keyRoles] = au.User.Roles()
if au.PermSession {
ses.Values[keyRememberMe] = true
} else {
delete(ses.Values, keyRememberMe)
}
}

View File

@@ -0,0 +1,136 @@
package request
import (
"context"
"html/template"
"net/http"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/gorilla/sessions"
)
type (
localeService interface {
NS(ctx context.Context, ns string) func(key string, rr ...string) string
}
// auth context simplifies auth request & response handling
AuthReq struct {
// HTTP request sent
Request *http.Request
Response http.ResponseWriter
// Sessions
Session *sessions.Session
// Authenticated user
AuthUser *authUser
// Current client (when in oauth2 flow)
Client *types.AuthClient
Locale localeService
// Redirect to
RedirectTo string
// Template to render
Template string
// Data to render with the template
Data map[string]interface{}
// handling flash alerts of all types
//
// should not be used for form errors; store them into sep. session keys
PrevAlerts, NewAlerts []Alert
// HTTP status to send
Status int
}
Alert struct {
// primary, secondary, danger, warning...
Type string
Text string
Html template.URL
}
// ExtraReqInfo serves as transport struct for additional
// request information we want to store with the oauth2 token
//
// There is effortless way to extend token info that is created inside go-oauth2 lib
// so we'll attach this struct to (request's) context with middleware (see MountHttpRoutes)
// and unpack from context when token is created in CortezaTokenStore.Create()
//
// ExtraReqInfo struct also serves as context value key!
ExtraReqInfo struct {
RemoteAddr string
UserAgent string
}
)
func (req AuthReq) Context() context.Context { return req.Request.Context() }
func (req *AuthReq) SetInternalError(err error) bool {
if err == nil {
return false
}
req.Status = http.StatusInternalServerError
req.Data["error"] = err
return true
}
func (req *AuthReq) PushAlert(text string) {
req.NewAlerts = append(req.NewAlerts, Alert{Type: "primary", Text: text})
}
func (req *AuthReq) PushDangerAlert(text string) {
req.NewAlerts = append(req.NewAlerts, Alert{Type: "danger", Text: text})
}
func (req *AuthReq) PopAlerts() []Alert {
val, has := req.Session.Values["alerts"]
if !has {
return nil
}
delete(req.Session.Values, "alerts")
return val.([]Alert)
}
func (req *AuthReq) SetAlerts(aa ...Alert) {
if len(aa) == 0 {
return
}
req.Session.Values["alerts"] = aa
}
// retrives key-value from session, stored under request-uri key
func (req *AuthReq) GetKV() map[string]string {
val, has := req.Session.Values["KV:"+req.Request.RequestURI]
if !has {
return nil
}
return val.(map[string]string)
}
// sets key-value value to session under request-uri key
func (req *AuthReq) SetKV(val map[string]string) {
if val == nil {
delete(req.Session.Values, "KV:"+req.Request.RequestURI)
} else {
req.Session.Values["KV:"+req.Request.RequestURI] = val
}
}
func (req *AuthReq) PopKV() map[string]string {
val := req.GetKV()
req.SetKV(nil)
return val
}

View File

@@ -0,0 +1,244 @@
package request
import (
"bytes"
"context"
"encoding/gob"
"fmt"
"net/http"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/pkg/rand"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
)
// cortezaSessionStore implements the session store and bridge to corteza store
type (
cortezaSessionStore struct {
// session store does not accept context on functions
// so we'll store the general one here on struct so we
// can capture termination signals..
//ctx context.Context
store store.AuthSessions
//cookies sessions.CookieStore
//stopCleanup chan bool
Codecs []securecookie.Codec
opt options.AuthOpt
}
)
var (
// wrapper around time.Now() that will aid service testing
now = func() *time.Time {
c := time.Now().Round(time.Second)
return &c
}
)
func init() {
gob.Register(map[string]string{})
}
func CortezaSessionStore(store store.AuthSessions, opt options.AuthOpt) *cortezaSessionStore {
var domain = opt.SessionCookieDomain
if strings.Contains(domain, ":") {
// do not set domain on the cookie if it contains port
domain = ""
}
return &cortezaSessionStore{
store: store,
Codecs: securecookie.CodecsFromPairs([]byte(opt.Secret)),
opt: opt,
}
}
func (s cortezaSessionStore) Get(r *http.Request, name string) (*sessions.Session, error) {
return sessions.GetRegistry(r).Get(s, name)
}
func (s cortezaSessionStore) New(r *http.Request, name string) (session *sessions.Session, err error) {
var (
domain = s.opt.SessionCookieDomain
)
if strings.Contains(domain, ":") {
// do not set domain on the cookie if it contains colon
// this most likely means it contains port
//
// browsers might misbehave
domain = ""
}
session = sessions.NewSession(s, name)
session.IsNew = true
session.Options = &sessions.Options{
Path: s.opt.SessionCookiePath,
Domain: domain,
Secure: s.opt.SessionCookieSecure,
HttpOnly: true,
}
if cook, errCookie := r.Cookie(name); errCookie == nil {
err = securecookie.DecodeMulti(name, cook.Value, &session.ID, s.Codecs...)
if err == nil {
err = s.load(r.Context(), session)
if err == nil {
session.IsNew = false
} else {
err = nil
}
}
}
return
}
func (s cortezaSessionStore) load(ctx context.Context, ses *sessions.Session) error {
cortezaSession, err := s.store.LookupAuthSessionByID(ctx, ses.ID)
if err != nil {
return err
}
if now().After(cortezaSession.ExpiresAt) {
return fmt.Errorf("session expired")
}
if err = gob.NewDecoder(bytes.NewReader(cortezaSession.Data)).Decode(&ses.Values); err != nil {
return fmt.Errorf("failed to decode session: %w", err)
}
// Store original session back for ref
ses.Values[keyOriginalSession] = cortezaSession
return nil
}
func (s cortezaSessionStore) Save(r *http.Request, w http.ResponseWriter, ses *sessions.Session) (err error) {
// Set delete if max-age is < 0
if ses.Options.MaxAge < 0 {
err = s.Delete(r.Context(), ses)
if err != nil {
return err
}
http.SetCookie(w, sessions.NewCookie(ses.Name(), "", ses.Options))
newSes, err := s.New(r, ses.Name())
if err != nil {
return err
}
ses.Options = newSes.Options
ses.IsNew = true
ses.ID = ""
ses.Values = nil
return nil
}
if len(ses.Values) == 0 && ses.IsNew {
// no values set, nothing to save
return nil
}
if ses.ID == "" || ses.IsNew {
ses.ID = string(rand.Bytes(64))
}
var (
// make a copy of options for cookie
cOpt = *ses.Options
// expiration for stored session
exp time.Time
)
if IsPermLogin(ses) {
// if permanent login, make sure max-age is recalculated
// so that it slides in the future with every new session-save
cOpt.MaxAge = int(s.opt.SessionPermLifetime / time.Second)
// recalculate expiration for the stored (db) session
exp = now().Add(s.opt.SessionPermLifetime)
} else {
// Warning!
// when session is not permanent, max-age is NOT set on the cookie
// making cookie last only for the length of the session
// recalculate expiration for the stored (db) session
// we do that even if the cookie does not have a value
exp = now().Add(s.opt.SessionLifetime)
}
if err = s.save(r.Context(), ses, exp); err != nil {
return err
}
// Keep the session ID key in a cookie so that it can be looked up in DB later.
encoded, err := securecookie.EncodeMulti(ses.Name(), ses.ID, s.Codecs...)
if err != nil {
return err
}
http.SetCookie(w, sessions.NewCookie(ses.Name(), encoded, &cOpt))
return nil
}
// save writes encoded session.Values to a database record.
// writes to http_sessions table by default.
func (s cortezaSessionStore) save(ctx context.Context, ses *sessions.Session, expires time.Time) (err error) {
var (
buf = &bytes.Buffer{}
cortezaSession *types.AuthSession
)
// retrieve original session value
if aux, is := ses.Values[keyOriginalSession]; is {
cortezaSession = aux.(*types.AuthSession)
} else {
cortezaSession = &types.AuthSession{
ID: ses.ID,
CreatedAt: *now(),
}
// new session does not belong to anyone yet.
// retrieve user id from ses. values
if au := GetAuthUser(ses); au != nil {
cortezaSession.UserID = au.User.ID
}
extra := auth.GetExtraReqInfoFromContext(ctx)
cortezaSession.UserAgent = extra.UserAgent
cortezaSession.RemoteAddr = extra.RemoteAddr
}
cortezaSession.ExpiresAt = expires
delete(ses.Values, keyOriginalSession)
if err = gob.NewEncoder(buf).Encode(ses.Values); err != nil {
return fmt.Errorf("failed to encode session: %w", err)
}
cortezaSession.Data = buf.Bytes()
return s.store.UpsertAuthSession(ctx, cortezaSession)
}
func (s cortezaSessionStore) Delete(ctx context.Context, ses *sessions.Session) error {
if len(ses.ID) > 0 {
return s.store.DeleteAuthSessionByID(ctx, ses.ID)
}
return nil
}
var _ sessions.Store = &cortezaSessionStore{}

View File

@@ -0,0 +1,60 @@
package request
import (
"context"
"net/http"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/gorilla/sessions"
"go.uber.org/zap"
)
type (
SessionManager struct {
cstore store.AuthSessions
sstore sessions.Store
opt options.AuthOpt
log *zap.Logger
}
)
func NewSessionManager(store store.AuthSessions, opt options.AuthOpt, log *zap.Logger) *SessionManager {
m := &SessionManager{opt: opt, log: log}
m.cstore = store
m.sstore = CortezaSessionStore(store, opt)
return m
}
func (m SessionManager) Store() sessions.Store { return m.sstore }
func (m *SessionManager) Get(r *http.Request) *sessions.Session {
ses, _ := m.sstore.Get(r, m.opt.SessionCookieName)
return ses
}
func (m *SessionManager) Save(w http.ResponseWriter, r *http.Request) {
if err := m.Get(r).Save(r, w); err != nil {
m.log.Warn("failed to save sessions", zap.Error(err))
}
}
// Returns all users sessions
func (m *SessionManager) Search(ctx context.Context, userID uint64) (set []*types.AuthSession, err error) {
set, _, err = m.cstore.SearchAuthSessions(ctx, types.AuthSessionFilter{UserID: userID})
for i := range set {
set[i].Data = nil
}
return
}
// DeleteByUserID removes session by user ID
func (m *SessionManager) DeleteByUserID(ctx context.Context, userID uint64) (err error) {
return m.cstore.DeleteAuthSessionsByUserID(ctx, userID)
}
// DeleteByID removes session by it's ID
func (m *SessionManager) DeleteByID(ctx context.Context, sessionID string) (err error) {
return m.cstore.DeleteAuthSessionByID(ctx, sessionID)
}

View File

@@ -0,0 +1,109 @@
package request
import (
"net/url"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/gorilla/sessions"
)
const (
keyRememberMe = "remember-me"
keyOriginalSession = "originalSession"
keyAuthUser = "authUser"
keyRoles = "roles"
keyOAuth2AuthParams = "oauth2AuthParams"
keyOAuth2Client = "oauth2ClientID"
keyOAuth2ClientAuthorized = "oauth2ClientAuthorized"
)
// GetUser is wrapper to get value from session
func GetAuthUser(ses *sessions.Session) *authUser {
val, has := ses.Values[keyAuthUser]
if !has {
return nil
}
au := val.(*authUser)
if au.User != nil {
au.User.SetRoles(getRoleMemberships(ses)...)
}
return au
}
// GetRoleMemberships is wrapper to get value from session
func getRoleMemberships(ses *sessions.Session) []uint64 {
val, has := ses.Values[keyRoles]
if !has {
return nil
}
return val.([]uint64)
}
// GetOAuth2AuthParams is wrapper to get value from session
func GetOAuth2AuthParams(ses *sessions.Session) url.Values {
val, has := ses.Values[keyOAuth2AuthParams]
if !has {
return nil
}
return val.(url.Values)
}
// SetOauth2AuthParams is a session value setting wrapper for Oauth2AuthParams
func SetOauth2AuthParams(ses *sessions.Session, val url.Values) {
if val != nil {
ses.Values[keyOAuth2AuthParams] = val
} else {
delete(ses.Values, keyOAuth2AuthParams)
}
}
// GetOauth2Client is wrapper to get value from session
func GetOauth2Client(ses *sessions.Session) *types.AuthClient {
val, has := ses.Values[keyOAuth2Client]
if !has {
return nil
}
return val.(*types.AuthClient)
}
// SetOauth2Client is a session value setting wrapper for Oauth2Client
func SetOauth2Client(ses *sessions.Session, val *types.AuthClient) {
if val != nil {
ses.Values[keyOAuth2Client] = val
} else {
delete(ses.Values, keyOAuth2Client)
}
}
// IsOauth2ClientAuthorized is wrapper to get value from session
func IsOauth2ClientAuthorized(ses *sessions.Session) bool {
val, has := ses.Values[keyOAuth2ClientAuthorized]
if !has {
return false
}
return val.(bool)
}
// SetOauth2ClientAuthorized is a session value setting wrapper for Oauth2ClientAuthorized
func SetOauth2ClientAuthorized(ses *sessions.Session, val bool) {
if val {
ses.Values[keyOAuth2ClientAuthorized] = true
} else {
delete(ses.Values, keyOAuth2ClientAuthorized)
}
}
// IsPermLogin decodes remember-me flag from the session and returns true if set
func IsPermLogin(ses *sessions.Session) (p bool) {
if aux, has := ses.Values[keyRememberMe]; has {
p, _ = aux.(bool)
}
return
}