Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: header auth #6825

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in utils.GeneratePassowrd(). It should be utils.GeneratePassword(). This could lead to a runtime error if the function does not exist.

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"`
}
Loading