-
Notifications
You must be signed in to change notification settings - Fork 368
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
Remote Authenticator Service & LDAP Removal #5285
Remote Authenticator Service & LDAP Removal #5285
Conversation
@nopcoder please jump in as well if you can take a look 🙏 |
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.
Thanks!
Nothing major requested :-)
cmd/lakefs/cmd/run.go
Outdated
externalAuthenticator, err := newExternalBasicAuthenticator(cfg, authService, logger) | ||
|
||
if err != nil { | ||
fmt.Printf("%s: %s\n", "failed to create external authenticator", err) |
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.
Please exit lakeFS from the main program. This function could just return an error, which would let us wrap it with additional information about why the LDAP authenticator got called.
Also if we did that then we would not be writing error messages to standard output as we end up doing here (although that is of course easy to fix at this place).
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 quite sure what you mean, can you take a look at the change I added? If not that then please elaborate.
Also, assuming I understand you correctly notice I applied the same change to allowForeign, err := cmd.Flags().GetBool(mismatchedReposFlagName)
in the same file as well.
docs/reference/authentication.md
Outdated
@@ -27,6 +27,12 @@ Web UI at Administration / Users to create users. Users have an access key | |||
for logging into the Web UI or authenticating programmatic requests to the API | |||
Server or the S3 Gateway. | |||
|
|||
#### Remote Authenticator Service | |||
|
|||
LakeFS server supports external authentication, the feature can be configured by providing an HTTP endpoint to an external authentication service. This integration can be especially useful if you already have an existing authentication system in place, as it allows you to reuse that system instead of maintaining a new one. |
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.
@ortz how much guidance do we plan for explaining external authentication?
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 see how many requests we got for this and we'll invest efforts accordingly.
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.
👍
|
||
// Expected response from the remote authenticator service | ||
type AuthenticationResponse struct { | ||
// optional, if returned then the user will be used as the official username in lakeFS |
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 that this is how LDAP works today. Isn't this planned to be the user's DN?
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.
Yes correct, but the field name is generic to different systems, not sure what's the question?
data, err := ra.doRequest(log, req) | ||
|
||
if err != nil { | ||
return "", fmt.Errorf("doing http request %s: %w", username, err) |
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.
return "", fmt.Errorf("doing http request %s: %w", username, err) | |
return "", fmt.Errorf("remote authenticate %s: %w", username, err) |
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 it's less indicative since it's less explicit on which step failed. there are many authenticators running (chain) with errors chaining when they occur.
I prefer to keep it (specifically the part about http)
cmd/lakefs/cmd/run.go
Outdated
@@ -25,6 +25,7 @@ import ( | |||
"github.com/treeverse/lakefs/pkg/auth" | |||
"github.com/treeverse/lakefs/pkg/auth/crypt" | |||
"github.com/treeverse/lakefs/pkg/auth/email" | |||
remoteauth "github.com/treeverse/lakefs/pkg/auth/remote_authenticator" |
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.
authremote
or same package - not sure we need a different package for this one.
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.
- Fixed
- Regarding another package, even though it's fairly small, the reason I prefer another package is due to encapsulation. In this particular case for a new comer like myself it's really confusing what code in
auth
suppose to do, authentication, authorization etc.
cmd/lakefs/cmd/run.go
Outdated
WithField("feature", "LDAP"). | ||
Warn("Enabling LDAP on lakeFS server, but this functionality is deprecated") | ||
middlewareAuthenticator = append(middlewareAuthenticator, newLDAPAuthenticator(cfg.Auth.LDAP, authService)) | ||
externalAuthenticator, err := newExternalBasicAuthenticator(cfg, authService, logger) |
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.
- Think we should drop the
Basic
from the name - it is a remote authenticator. So maybe call it - We have external and remote - I assume this func was made to consolidate LDAP and Remote. It the code looks more complex as the previous code was simple: check the configuration and chain the auth to the middleware.
- Suggest: moving the building of the auth middleware chain to a function (not partial). Check the configuration validity as part of Config, so the code here will just allocate the right middleware based on verified configuration.
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.
it's now all in the same place like LDAP used to be.
if err != nil { | ||
log.WithError(err).Error("failed creating request") | ||
return "", auth.ErrInvalidRequest | ||
} |
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 just long the err in this case and throw the error - I suggest to return the original in this case. Alt. will be to wrap it by invalid request, but I don't see the point in this case.
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.
👍
if err != nil { | ||
return "", fmt.Errorf("add newly created remote auth user %s to %s: %w", dbUsername, ra.Config.DefaultUserGroup, err) | ||
} | ||
return newUser.Username, nil |
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.
Use dbUsername
while we use it all the way until here. Or use newUser.Username
for all places we used dbUsername
.
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.
Done!
} | ||
|
||
func (la *RemoteAuthenticator) String() string { | ||
return RemoteAuthSource |
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.
Do we need to serialize remote authenticator as string?
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.
Yes, it's used by the chain authenticator.
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.
Thanks @Isan-Rivkin !
Overall, looks good to me... I do have some questions
docs/reference/authentication.md
Outdated
|
||
lakeFS server supports external authentication, the feature can be configured by providing an HTTP endpoint to an external authentication service. This integration can be especially useful if you already have an existing authentication system in place, as it allows you to reuse that system instead of maintaining a new one. | ||
|
||
TBD |
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.
leftover? or you plan on adding content here?
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 a left over, just not sure how deep to do see
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.
updated all relevant docs.
pkg/config/config.go
Outdated
LDAP *LDAP | ||
OIDC OIDC | ||
RemoteAuthenticator *RemoteAuthenticator `mapstructure:"remote_authenticator"` | ||
OIDC OIDC `mapstructure:"oidc"` |
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.
is this relevant?
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.
Yes, The OIDC itself config is relevant. (From previous PR's)
here it's only mapstructure:"oidc"
following comment previously about alignment incase field name change.
_, err = ra.AuthService.CreateCredentials(ctx, newUser.Username) | ||
if err != nil { | ||
return "", fmt.Errorf("create credentials for remote auth user %s: %w", newUser.Username, err) | ||
} |
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 do we need to create credentials for that user?
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.
Actually it's a good question, right now it's backward compatible with the LDAP implementation.
I couldn't find a reason for it, testing without this function CreateCredentials
seems to work.
Do you happen to know why it's here @arielshaqed @nopcoder ?
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 don't remember why we did it this way. I think I didn't want to special-case the existing auth path, which needed to find some credentials or something??!?
Anyway if it works then we're well rid of it >:-)
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.
removed!
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.
Thanks!
Minor points, mostly about logging...
_, err = ra.AuthService.CreateCredentials(ctx, newUser.Username) | ||
if err != nil { | ||
return "", fmt.Errorf("create credentials for remote auth user %s: %w", newUser.Username, err) | ||
} |
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 don't remember why we did it this way. I think I didn't want to special-case the existing auth path, which needed to find some credentials or something??!?
Anyway if it works then we're well rid of it >:-)
pkg/config/config.go
Outdated
@@ -163,8 +166,8 @@ type Config struct { | |||
Token string | |||
SupportsInvites bool `mapstructure:"supports_invites"` | |||
} | |||
LDAP *LDAP | |||
OIDC OIDC | |||
RemoteAuthenticator *RemoteAuthenticator `mapstructure:"remote_authenticator"` |
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.
RemoteAuthenticator have Enabled property - suggest to remove the pointer from here. It will make handling the configuration less issue in case it is nil.
We only need to check cfg.RemoteAuthenticator.Enabled
Note - I'll also embed the type here as the complete configuration is easy to understand without jumping over the types.
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.
👍 as discussed
pkg/config/config.go
Outdated
UsernameAttribute string `mapstructure:"username_attribute"` | ||
UserBaseDN string `mapstructure:"user_base_dn"` | ||
UserFilter string `mapstructure:"user_filter"` | ||
// RemoteAuthenticator holds remote authentication configuration. |
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.
Just note that in the release notes of this release we should guide ppl to update the configuration as lakeFS will not run with known configuration properties - and we are removing LDAP.
So starting lakeFS with the old configuration will break.
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.
👍
client *http.Client | ||
} | ||
|
||
func NewRemoteAuthenticator(conf *config.RemoteAuthenticator, authService auth.Service, logger logging.Logger) (auth.Authenticator, error) { |
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.
- Return the concrete type and not the interface
- Remove dependency in config as possible - create a structure with the same layout as your configuration and just pass it while calling NewRemoteAuthenticator from your main. Alternative: pass the relevant parameters to configuration struct that you define as part of this package. This is to decouple of use of config package from this one.
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.
Return the concrete type and not the interface
, updated but for clarification, WHY?- Done
} | ||
|
||
func (ra *RemoteAuthenticator) String() string { | ||
return RemoteAuthSource |
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 do we need this one?
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 pretty sure I answered this and somehow my comment disappeared 😮
Anyway, it's used by chain auth when auth errors occurs.
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.
yes, I missed your answer.
// if the external authentication service provided an external user identifier, use it as the username | ||
|
||
if res.ExternalUserIdentifier != nil && *res.ExternalUserIdentifier != "" { | ||
log = ra.Logger.WithField("external_user_identifier", *res.ExternalUserIdentifier) |
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.
Use log
and not ra
as you like to add the extra information and not re-write
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.
Good catch thanks!
// if the external authentication service provided an external user identifier, use it as the username | ||
|
||
if res.ExternalUserIdentifier != nil && *res.ExternalUserIdentifier != "" { | ||
log = ra.Logger.WithField("external_user_identifier", *res.ExternalUserIdentifier) | ||
dbUsername = *res.ExternalUserIdentifier | ||
} |
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.
// if the external authentication service provided an external user identifier, use it as the username | |
if res.ExternalUserIdentifier != nil && *res.ExternalUserIdentifier != "" { | |
log = ra.Logger.WithField("external_user_identifier", *res.ExternalUserIdentifier) | |
dbUsername = *res.ExternalUserIdentifier | |
} | |
// if the external authentication service provided an external user identifier, use it as the username | |
externalUserIdentifier := swag.String(res.ExternalUserIdentifier) | |
if externalUserIdentifier != "" { | |
log = log.WithField("external_user_identifier", externalUserIdentifier) | |
dbUsername = externalUserIdentifier | |
} |
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.
👍 added (with swag.StringValue)
const DefaultRequestTimeout = 7 * time.Second | ||
const RemoteAuthSource = "remote_authenticator" |
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.
- use scope
const
- suggest to use https://github.com/mvdan/gofumpt
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 I see the benefit of adding gofumpt, but changed scope
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.
Its alternative to gofmt (command line) that will format your code just in strict way
|
||
func NewRemoteAuthenticator(conf RemoteAuthenticatorConfig, authService auth.Service, logger logging.Logger) (*RemoteAuthenticator, error) { | ||
if conf.Endpoint == "" { | ||
return nil, fmt.Errorf("base URL is empty: %w", ErrBadConfig) |
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.
Endpoint is empty / missing
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.
done
pkg/config/defaults.go
Outdated
@@ -47,6 +47,10 @@ func setDefaults(local bool) { | |||
viper.SetDefault("auth.ui_config.login_failed_message", "The credentials don't match.") | |||
viper.SetDefault("auth.ui_config.login_cookie_names", "internal_auth_session") | |||
|
|||
viper.SetDefault("auth.remote_authenticator.enabled", false) |
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.
No need to set default to the flag's default
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.
💯
pkg/config/defaults.go
Outdated
@@ -47,6 +47,10 @@ func setDefaults(local bool) { | |||
viper.SetDefault("auth.ui_config.login_failed_message", "The credentials don't match.") | |||
viper.SetDefault("auth.ui_config.login_cookie_names", "internal_auth_session") | |||
|
|||
viper.SetDefault("auth.remote_authenticator.enabled", false) | |||
viper.SetDefault("auth.remote_authenticator.default_user_group", "Viewers") | |||
viper.SetDefault("auth.remote_authenticator.request_timeout", 7*time.Second) |
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 prefer the to have a default to be 10 sec or 30 sec - something smaller than 'no timeout' but something that will work for many use-cases and in case the user will want to restrict the time for authenticate, it can be done by configuration.
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.
Previously (In LDAP) was 7, changed to 10 👍
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.
LGTM!
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.
Great job!
I do have some comments, some small fixes and questions
@@ -47,6 +47,9 @@ func setDefaults(local bool) { | |||
viper.SetDefault("auth.ui_config.login_failed_message", "The credentials don't match.") | |||
viper.SetDefault("auth.ui_config.login_cookie_names", "internal_auth_session") | |||
|
|||
viper.SetDefault("auth.remote_authenticator.default_user_group", "Viewers") |
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.
note that lakefs' default group is Developers, I think that we should be aligned with the non-remote authenticator configuration.
Closes #5262