Skip to content

Commit

Permalink
graph: Allow provisioning users with legacy names (#5255)
Browse files Browse the repository at this point in the history
Via configuration you can now configure to skip the validation of username and
instead decide to trust the upstream system that is adding users.
  • Loading branch information
Daniel Swärd authored and rhafer committed Jan 18, 2023
1 parent 7c94527 commit 96239af
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 9 deletions.
3 changes: 2 additions & 1 deletion services/graph/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ type Identity struct {

// API represents API configuration parameters.
type API struct {
GroupMembersPatchLimit int `yaml:"group_members_patch_limit" env:"GRAPH_GROUP_MEMBERS_PATCH_LIMIT" desc:"The amount of group members allowed to be added with a single patch request."`
GroupMembersPatchLimit int `yaml:"group_members_patch_limit" env:"GRAPH_GROUP_MEMBERS_PATCH_LIMIT" desc:"The amount of group members allowed to be added with a single patch request."`
UsernameMatch string `yaml:"graph_username_match" env:"GRAPH_USERNAME_MATCH" desc:"Option to allow legacy usernames. Supported options are 'default' and 'none'."`
}

// Events combines the configuration options for the event bus.
Expand Down
1 change: 1 addition & 0 deletions services/graph/pkg/config/defaults/defaultconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func DefaultConfig() *config.Config {
},
API: config.API{
GroupMembersPatchLimit: 20,
UsernameMatch: "default",
},
Reva: shared.DefaultRevaConfig(),
Spaces: config.Spaces{
Expand Down
10 changes: 10 additions & 0 deletions services/graph/pkg/config/parser/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,15 @@ func Validate(cfg *config.Config) error {
"graph", defaults2.BaseConfigPath())
}

switch cfg.API.UsernameMatch {
case "default", "none":
default:
return fmt.Errorf("The username match validator is invalid for %s. "+
"Make sure your %s config contains the proper values "+
"(e.g. by running ocis init or setting it manually in "+
"the config/corresponding environment variable).",
"graph", defaults2.BaseConfigPath())
}

return nil
}
2 changes: 1 addition & 1 deletion services/graph/pkg/service/v0/educationuser.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func (g Graph) PostEducationUser(w http.ResponseWriter, r *http.Request) {
}

if accountName, ok := u.GetOnPremisesSamAccountNameOk(); ok {
if !isValidUsername(*accountName) {
if !g.isValidUsername(*accountName) {
logger.Debug().Str("username", *accountName).Msg("could not create education user: username must be at least the local part of an email")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("username %s must be at least the local part of an email", *u.OnPremisesSamAccountName))
return
Expand Down
26 changes: 19 additions & 7 deletions services/graph/pkg/service/v0/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func (g Graph) PostUser(w http.ResponseWriter, r *http.Request) {
return
}
if accountName, ok := u.GetOnPremisesSamAccountNameOk(); ok {
if !isValidUsername(*accountName) {
if !g.isValidUsername(*accountName) {
logger.Debug().Str("username", *accountName).Msg("could not create user: username must be at least the local part of an email")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("username %s must be at least the local part of an email", *u.OnPremisesSamAccountName))
return
Expand Down Expand Up @@ -530,16 +530,28 @@ func (g Graph) PatchUser(w http.ResponseWriter, r *http.Request) {

}

// We want to allow email addresses as usernames so they show up when using them in ACLs on storages that allow integration with our glauth LDAP service
// so we are adding a few restrictions from https://stackoverflow.com/questions/6949667/what-are-the-real-rules-for-linux-usernames-on-centos-6-and-rhel-6
// names should not start with numbers
var usernameRegex = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]*(@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)*$")
const (
usernameMatchDefault = "default"
usernameMatchNone = "none"
)

var usernameRegexes = map[string]*regexp.Regexp{
// We want to allow email addresses as usernames so they show up when using them in ACLs on storages that allow integration with our glauth LDAP service
// so we are adding a few restrictions from https://stackoverflow.com/questions/6949667/what-are-the-real-rules-for-linux-usernames-on-centos-6-and-rhel-6
// names should not start with numbers
usernameMatchDefault: regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]*(@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)*$"),

func isValidUsername(e string) bool {
// In some cases users will be provisioned from an existing system, which may or may not have strange usernames. Because of this we want to "trust" the
// upstream system and allow weird usernames, so relying on the used identity provider to complain if a username is violating its restrictions.
usernameMatchNone: regexp.MustCompile(".*"),
}

func (g Graph) isValidUsername(e string) bool {
if len(e) < 1 && len(e) > 254 {
return false
}
return usernameRegex.MatchString(e)

return usernameRegexes[g.config.API.UsernameMatch].MatchString(e)
}

// regex from https://www.w3.org/TR/2016/REC-html51-20161101/sec-forms.html#valid-e-mail-address
Expand Down
57 changes: 57 additions & 0 deletions services/graph/pkg/service/v0/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,63 @@ var _ = Describe("Users", func() {

Expect(rr.Code).To(Equal(http.StatusOK))
})

Describe("Handling usernames with spaces", func() {
var (
newSvc = func(usernameMatch string) service.Service {
localCfg := defaults.FullDefaultConfig()
localCfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests
localCfg.TokenManager.JWTSecret = "loremipsum"
localCfg.Commons = &shared.Commons{}
localCfg.GRPCClientTLS = &shared.GRPCClientTLS{}

localCfg.API.UsernameMatch = usernameMatch

_ = ogrpc.Configure(ogrpc.GetClientOptions(cfg.GRPCClientTLS)...)
localSvc, _ := service.NewService(
service.Config(localCfg),
service.WithGatewayClient(gatewayClient),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),
service.WithRoleService(roleService),
)

return localSvc
}
)

BeforeEach(func() {
user.SetOnPremisesSamAccountName("username with spaces")
})

It("rejects a username with spaces if match regex is default", func() {
userJson, err := json.Marshal(user)
Expect(err).ToNot(HaveOccurred())

r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/users", bytes.NewBuffer(userJson))
r = r.WithContext(revactx.ContextSetUser(ctx, currentUser))

newSvc("default").PostUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})

It("creates a user with spaces in username if regex is none", func() {
roleService.On("AssignRoleToUser", mock.Anything, mock.Anything).Return(&settings.AssignRoleToUserResponse{}, nil)
identityBackend.On("CreateUser", mock.Anything, mock.Anything).Return(func(ctx context.Context, user libregraph.User) *libregraph.User {
user.SetId("/users/user")
return &user
}, nil)
userJson, err := json.Marshal(user)
Expect(err).ToNot(HaveOccurred())

r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/users", bytes.NewBuffer(userJson))
r = r.WithContext(revactx.ContextSetUser(ctx, currentUser))
newSvc("none").PostUser(rr, r)

Expect(rr.Code).To(Equal(http.StatusOK))
})
})

})

Describe("DeleteUser", func() {
Expand Down

0 comments on commit 96239af

Please sign in to comment.