Skip to content

Commit

Permalink
feat: header auth
Browse files Browse the repository at this point in the history
  • Loading branch information
mgilham committed Jan 17, 2025
1 parent fdcdbf0 commit cd32064
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 11 deletions.
5 changes: 4 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ indent_size = 2
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2
indent_size = 2

[*.{ts,tsx}]
indent_style = tab
2 changes: 1 addition & 1 deletion ee/query-service/app/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (ah *APIHandler) loginUser(w http.ResponseWriter, r *http.Request) {
}

// if all looks good, call auth
resp, err := baseauth.Login(ctx, &req)
resp, err := baseauth.Login(ctx, &req, nil)
if ah.HandleError(w, err, http.StatusUnauthorized) {
return
}
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/container/Login/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function Login({

const [precheckInProcess, setPrecheckInProcess] = useState(false);
const [precheckComplete, setPrecheckComplete] = useState(false);
const [headerAuthEmail, setHeaderAuthEmail] = useState<string | null>(null);

const { notifications } = useNotifications();

Expand All @@ -64,11 +65,14 @@ function Login({
getUserVersionResponse.data &&
getUserVersionResponse.data.payload
) {
const { setupCompleted } = getUserVersionResponse.data.payload;
const { setupCompleted, headerEmail } = getUserVersionResponse.data.payload;
if (!setupCompleted) {
// no org account registered yet, re-route user to sign up first
history.push(ROUTES.SIGN_UP);
}
if (headerEmail) {
setHeaderAuthEmail(headerEmail);
}
}
}, [getUserVersionResponse]);

Expand Down Expand Up @@ -186,6 +190,12 @@ function Login({
}
};

useEffect(() => {
form.setFieldValue('email', headerAuthEmail);
setPrecheckComplete(true);
form.submit();
}, [headerAuthEmail, form]);

const renderSAMLAction = (): JSX.Element => (
<Button
type="primary"
Expand Down
1 change: 1 addition & 0 deletions frontend/src/types/api/user/getVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export interface PayloadProps {
version: string;
ee: 'Y' | 'N';
setupCompleted: boolean;
headerEmail: string;
}
7 changes: 6 additions & 1 deletion pkg/query-service/app/http_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1897,10 +1897,15 @@ func (aH *APIHandler) getDisks(w http.ResponseWriter, r *http.Request) {

func (aH *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
version := version.GetVersion()
var headerEmail string
if user := auth.GetUserFromHeader(r); user != nil {
headerEmail = user.Email
}
versionResponse := model.GetVersionResponse{
Version: version,
EE: "Y",
SetupCompleted: aH.SetupCompleted,
HeaderEmail: headerEmail,
}

aH.WriteJSON(w, r, versionResponse)
Expand Down Expand Up @@ -2112,7 +2117,7 @@ func (aH *APIHandler) loginUser(w http.ResponseWriter, r *http.Request) {
// req.RefreshToken = c.Value
// }

resp, err := auth.Login(context.Background(), req)
resp, err := auth.Login(context.Background(), req, auth.GetUserFromHeader(r))
if aH.HandleError(w, err, http.StatusUnauthorized) {
return
}
Expand Down
74 changes: 70 additions & 4 deletions pkg/query-service/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ const (
minimumPasswordLength = 8
)

const (
HeaderAuthEmail = "X-Signoz-User"
HeaderAuthRole = "X-Signoz-Role"
HeaderAuthOrg = "X-Signoz-Org"
)

var UseHeaderAuth bool

var (
ErrorInvalidCreds = fmt.Errorf("invalid credentials")
)
Expand Down Expand Up @@ -532,6 +540,52 @@ func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword b
return user, nil
}

func CreateHeaderAuthUser(ctx context.Context, headerUser model.UserPayload) (*model.User, error) {

if headerUser.Email == "" || headerUser.Role == "" || headerUser.Organization == "" {
return nil, errors.New("all of email, role, and organization must be specified when logging in a new user via header")
}

org, apierr := dao.DB().GetOrgByName(ctx, headerUser.Organization)
if apierr != nil {
zap.L().Error("GetOrgByName failed", zap.Error(apierr.ToError()))
return nil, apierr
}
if org == nil {
org, apierr = dao.DB().CreateOrg(ctx,
&model.Organization{Name: headerUser.Organization, IsAnonymous: false, HasOptedUpdates: false})
if apierr != nil {
zap.L().Error("CreateOrg failed", zap.Error(apierr.ToError()))
return nil, apierr
}
}

group, apiErr := dao.DB().GetGroupByName(ctx, headerUser.Role)
if apiErr != nil {
zap.L().Error("GetGroupByName failed", zap.Error(apiErr.Err))
return nil, apiErr
}

hash, err := PasswordHash(utils.GeneratePassowrd())
if err != nil {
zap.L().Error("failed to generate password hash when registering a user", zap.Error(err))
return nil, model.InternalError(model.ErrSignupFailed{})
}

user := &model.User{
Id: uuid.NewString(),
Name: headerUser.Email,
Email: headerUser.Email,
Password: hash,
CreatedAt: time.Now().Unix(),
ProfilePictureURL: "", // Currently unused
GroupId: group.Id,
OrgId: org.Id,
}

return dao.DB().CreateUser(ctx, user, false)
}

// Register registers a new user. For the first register request, it doesn't need an invite token
// and also the first registration is an enforced ADMIN registration. Every subsequent request will
// need an invite token to go through.
Expand All @@ -550,10 +604,10 @@ func Register(ctx context.Context, req *RegisterRequest) (*model.User, *model.Ap
}

// Login method returns access and refresh tokens on successful login, else it errors out.
func Login(ctx context.Context, request *model.LoginRequest) (*model.LoginResponse, error) {
func Login(ctx context.Context, request *model.LoginRequest, headerUser *model.UserPayload) (*model.LoginResponse, error) {
zap.L().Debug("Login method called for user", zap.String("email", request.Email))

user, err := authenticateLogin(ctx, request)
user, err := authenticateLogin(ctx, request, headerUser)
if err != nil {
zap.L().Error("Failed to authenticate login request", zap.Error(err))
return nil, err
Expand All @@ -577,7 +631,7 @@ func Login(ctx context.Context, request *model.LoginRequest) (*model.LoginRespon
}

// authenticateLogin is responsible for querying the DB and validating the credentials.
func authenticateLogin(ctx context.Context, req *model.LoginRequest) (*model.UserPayload, error) {
func authenticateLogin(ctx context.Context, req *model.LoginRequest, headerUser *model.UserPayload) (*model.UserPayload, error) {

// If refresh token is valid, then simply authorize the login request.
if len(req.RefreshToken) > 0 {
Expand All @@ -597,7 +651,19 @@ func authenticateLogin(ctx context.Context, req *model.LoginRequest) (*model.Use
if err != nil {
return nil, errors.Wrap(err.Err, "user not found")
}
if user == nil || !passwordMatch(user.Password, req.Password) {
if UseHeaderAuth && headerUser != nil {
if user == nil {
// create user specified in header that doesn't already exist in DB
_, createerr := CreateHeaderAuthUser(ctx, *headerUser)
if createerr != nil {
return nil, errors.Wrap(createerr, "creating new user from header")
}
user, err = dao.DB().GetUserByEmail(ctx, req.Email)
if err != nil {
return nil, errors.Wrap(err.Err, "new user not found")
}
}
} else if user == nil || !passwordMatch(user.Password, req.Password) {
return nil, ErrorInvalidCreds
}
return user, nil
Expand Down
16 changes: 16 additions & 0 deletions pkg/query-service/auth/rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ func GetUserFromRequest(r *http.Request) (*model.UserPayload, error) {
return user, nil
}

func GetUserFromHeader(r *http.Request) *model.UserPayload {
email := r.Header.Get(HeaderAuthEmail)
if email == "" {
return nil
}
role := r.Header.Get(HeaderAuthRole)
org := r.Header.Get(HeaderAuthOrg)
return &model.UserPayload{
User: model.User{
Email: email,
},
Role: role,
Organization: org,
}
}

func IsSelfAccessRequest(user *model.UserPayload, id string) bool { return user.Id == id }

func IsViewer(user *model.UserPayload) bool { return user.GroupId == AuthCacheObj.ViewerGroupId }
Expand Down
7 changes: 4 additions & 3 deletions pkg/query-service/dao/sqlite/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,10 @@ func (mds *ModelDaoSqlite) initializeOrgPreferences(ctx context.Context) error {
return apiError.Err
}

if len(orgs) > 1 {
return errors.Errorf("Found %d organizations, expected one or none.", len(orgs))
}
// TODO(mlg): reconsider how to handle this
//if len(orgs) > 1 {
// return errors.Errorf("Found %d organizations, expected one or none.", len(orgs))
//}

var org model.Organization
if len(orgs) == 1 {
Expand Down
5 changes: 5 additions & 0 deletions pkg/query-service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ func main() {
var maxOpenConns int
var dialTimeout time.Duration

var useHeaderAuth bool

flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces")
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
Expand All @@ -65,6 +67,7 @@ func main() {
flag.IntVar(&maxIdleConns, "max-idle-conns", 50, "(number of connections to maintain in the pool, only used with clickhouse if not set in ClickHouseUrl env var DSN.)")
flag.IntVar(&maxOpenConns, "max-open-conns", 100, "(max connections for use at any time, only used with clickhouse if not set in ClickHouseUrl env var DSN.)")
flag.DurationVar(&dialTimeout, "dial-timeout", 5*time.Second, "(the maximum time to establish a connection, only used with clickhouse if not set in ClickHouseUrl env var DSN.)")
flag.BoolVar(&useHeaderAuth, "use-header-auth", false, "use HTTP header from a trusted proxy for authentication")
flag.Parse()

loggerMgr := initZapLog()
Expand Down Expand Up @@ -120,6 +123,8 @@ func main() {
logger.Fatal("Failed to initialize auth cache", zap.Error(err))
}

auth.UseHeaderAuth = useHeaderAuth

signalsChannel := make(chan os.Signal, 1)
signal.Notify(signalsChannel, os.Interrupt, syscall.SIGTERM)

Expand Down
1 change: 1 addition & 0 deletions pkg/query-service/model/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -690,4 +690,5 @@ type GetVersionResponse struct {
Version string `json:"version"`
EE string `json:"ee"`
SetupCompleted bool `json:"setupCompleted"`
HeaderEmail string `json:"headerEmail"`
}

0 comments on commit cd32064

Please sign in to comment.