-
Notifications
You must be signed in to change notification settings - Fork 11.6k
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
Limit concurrent user sessions #27655
Limit concurrent user sessions #27655
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comments
pkg/services/auth/auth_token.go
Outdated
) | ||
|
||
func (s *UserAuthTokenService) isConcurrentSessionLimitEnabled() bool { | ||
return s.Cfg.FeatureToggles[concurrentSessionLimitKey] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why feature toggle and not a setting in [auth]?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not really sure about this. Maybe @mjseaman can give us with more details.
The issue definition specifies it as such, I suppose it's somehow related with customer requirements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feature toggles are only used for temporary features that eventually will be enabled/included per default. Usually used when developing new experimental features. Adding it as a separate setting is basically a toggle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed with @marefr, we don't need feature toggle in this case, this is not an experimental feature.
pkg/services/auth/auth_token.go
Outdated
@@ -404,6 +410,35 @@ func (s *UserAuthTokenService) rotatedAfterParam() int64 { | |||
return getTime().Add(-s.Cfg.LoginMaxInactiveLifetime).Unix() | |||
} | |||
|
|||
const ( | |||
concurrentSessionLimit = 3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would rather have this as a setting in [auth]. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's wait for Mitch to address this.
But yup, I agree that moving this constant into a configuration variable could be a good idea 👌
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we shouldn't change default behavior when introducing new features. So I'd prefer to have two config options: one for enabling user session limit and second for setting limit, like:
[auth]
enable_user_session_limit = false
user_session_limit = 10
Or maybe even just one config option user_session_limit
which is 0
by default and disables limit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on earlier setting like login_cookie_name
, login_maximum_inactive_lifetime_duration
I would suggest login_maximum_concurrent_user_sessions
that default to 0 (disabled).
pkg/services/auth/auth_token.go
Outdated
} | ||
|
||
return s.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { | ||
result, err := dbSession.Exec("DELETE FROM user_auth_token WHERE id NOT IN (SELECT id FROM user_auth_token WHERE seen_at <> 0 ORDER BY seen_at DESC LIMIT ?) AND seen_at <> 0;", concurrentSessionLimit-1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should filter by user id here. Couldn't this query be written with only delete clause/without in(...)
, maybe not possible with order by then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Completely agree, I already figured out the filter by user id missing but I was waiting for feedback to do it. Anyway, thanks for pointing it, it helped me to do not forget it.
Regarding the query, I cannot see a simpler way to do it. Probably, the subquery might be replaced by a JOIN clause but not really sure if in a simpler / clearer way. Feel free to write down an example if you have something in mind, I'd really appreciate it.
Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related with the query ☝️ I've already been exploring the approach using something like DELETE ... LIMIT ... OFFSET
which might be enough for our case and also seem to be supported by SQLite (also by PosgreSQL & MySQL I guess), but it didn't seem to be supported by the Go client.
pkg/services/auth/auth_token.go
Outdated
@@ -404,6 +410,35 @@ func (s *UserAuthTokenService) rotatedAfterParam() int64 { | |||
return getTime().Add(-s.Cfg.LoginMaxInactiveLifetime).Unix() | |||
} | |||
|
|||
const ( | |||
concurrentSessionLimit = 3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we shouldn't change default behavior when introducing new features. So I'd prefer to have two config options: one for enabling user session limit and second for setting limit, like:
[auth]
enable_user_session_limit = false
user_session_limit = 10
Or maybe even just one config option user_session_limit
which is 0
by default and disables limit.
pkg/services/auth/auth_token.go
Outdated
) | ||
|
||
func (s *UserAuthTokenService) isConcurrentSessionLimitEnabled() bool { | ||
return s.Cfg.FeatureToggles[concurrentSessionLimitKey] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed with @marefr, we don't need feature toggle in this case, this is not an experimental feature.
pkg/services/auth/auth_token.go
Outdated
} | ||
|
||
func (s *UserAuthTokenService) revokeOldUserTokens(ctx context.Context, userId int64) error { | ||
if !s.isConcurrentSessionLimitEnabled() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't better to check this in CreateToken()
function? In that case it looks more straightforward and clear I think.
After discussing it with @xlson, the initial approach has been changed. So, the concurrent user session limits are finally implemented as an extensible mechanism that has not limits by default but it can be overwritten when needed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logic looks good, but I think we're missing an index for user_id on the user_auth_token table.
( | ||
SELECT id | ||
FROM user_auth_token | ||
WHERE user_id = ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't have an index for user_id
in
grafana/pkg/services/sqlstore/migrations/user_auth_token_mig.go
Lines 8 to 27 in b0df728
userAuthTokenV1 := Table{ | |
Name: "user_auth_token", | |
Columns: []*Column{ | |
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, | |
{Name: "user_id", Type: DB_BigInt, Nullable: false}, | |
{Name: "auth_token", Type: DB_NVarchar, Length: 100, Nullable: false}, | |
{Name: "prev_auth_token", Type: DB_NVarchar, Length: 100, Nullable: false}, | |
{Name: "user_agent", Type: DB_NVarchar, Length: 255, Nullable: false}, | |
{Name: "client_ip", Type: DB_NVarchar, Length: 255, Nullable: false}, | |
{Name: "auth_token_seen", Type: DB_Bool, Nullable: false}, | |
{Name: "seen_at", Type: DB_Int, Nullable: true}, | |
{Name: "rotated_at", Type: DB_Int, Nullable: false}, | |
{Name: "created_at", Type: DB_Int, Nullable: false}, | |
{Name: "updated_at", Type: DB_Int, Nullable: false}, | |
}, | |
Indices: []*Index{ | |
{Cols: []string{"auth_token"}, Type: UniqueIndex}, | |
{Cols: []string{"prev_auth_token"}, Type: UniqueIndex}, | |
}, | |
} |
@marefr wdyt, should we add an index here or is there a better way to go about this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting one! Let's wait for @marefr point of view, but it might have sense since filtering by user_id
will still be required, I guess 🤔
Thanks!
PR significantly changed
ORDER BY seen_at DESC | ||
LIMIT ? | ||
) | ||
AND user_id = ? | ||
AND seen_at != 0;` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure about the logic for seen_at
. Maybe moving this above inserting a new token would simplify things? Doing that could do a query to receive all auth tokens for user and then iterate and find the oldest (based on created_at or rotated_at) above maxConcurrentSessions-1
token and finally delete from user_auth_token where id in(<list of id's>)
.
Doing this you could potentially move all of this logic to enterprise by wrapping and overriding the UserTokenService
in similar manner as done with the data source cache service using registry.RegisterOverride (in enterprise). @joanlopez @xlson thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup! I also thought about it, but I did it before just to avoid removing an existing session with the possibility of having issues while creating the new one. But this might be something insignificant for this case. So, if it helps us to simplify the code, I agree we can do it the other way round.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing this you could potentially move all of this logic to enterprise by wrapping and overriding the UserTokenService ª in similar manner as done with the data source cache service using registry.RegisterOverride (in enterprise). @joanlopez @xlson thoughts?
This approach produces inject
errors because the models.UserTokenService
interface is injected into some other services and once both implementations are present it does not know which one should be injected.
Any idea? 🤔 cc/ @marefr @xlson @sakjur
Thank you so much!!! 🙏
I proceed to close it since it's outdated and no longer valid. The major outcome from this discussion was #27908. Thanks guys!!! 👍 |
What this PR does / why we need it:
It introduces an extensible way to limit the maximum number of concurrent sessions per user.
Special notes for your reviewer: