@@ -103,10 +103,65 @@ pub struct User {
103103 pub protected : bool ,
104104}
105105
106+ /// Accept the UI's existing roles array or an object with a caller-supplied password.
107+ #[ derive( Deserialize ) ]
108+ #[ serde( untagged) ]
109+ pub enum CreateUserRequest {
110+ Roles ( HashSet < String > ) ,
111+ Options ( CreateUserOptions ) ,
112+ }
113+
114+ #[ derive( Deserialize ) ]
115+ #[ serde( rename_all = "camelCase" , deny_unknown_fields) ]
116+ pub struct CreateUserOptions {
117+ #[ serde( default ) ]
118+ roles : HashSet < String > ,
119+ provided_password : Option < String > ,
120+ }
121+
122+ impl CreateUserRequest {
123+ pub fn split_request ( self ) -> ( HashSet < String > , Option < String > ) {
124+ match self {
125+ Self :: Roles ( roles) => ( roles, None ) ,
126+ Self :: Options ( options) => ( options. roles , options. provided_password ) ,
127+ }
128+ }
129+ }
130+
106131impl User {
107132 // create a new User and return self with password generated for said user.
108133 pub fn new_basic ( username : String , tenant : Option < String > , protected : bool ) -> ( Self , String ) {
109- let PassCode { password, hash } = Basic :: gen_new_password ( ) ;
134+ Self :: new_basic_with_passcode ( username, tenant, protected, Basic :: gen_new_password ( ) )
135+ }
136+
137+ pub fn new_basic_with_provided_password (
138+ username : String ,
139+ tenant : Option < String > ,
140+ protected : bool ,
141+ password : String ,
142+ ) -> Result < ( Self , String ) , RBACError > {
143+ if !( 8 ..=256 ) . contains ( & password. len ( ) )
144+ || password. trim ( ) != password
145+ || password. chars ( ) . any ( char:: is_control)
146+ {
147+ return Err ( RBACError :: InvalidProvidedPassword ) ;
148+ }
149+
150+ let hash = gen_hash ( & password) ;
151+ Ok ( Self :: new_basic_with_passcode (
152+ username,
153+ tenant,
154+ protected,
155+ PassCode { password, hash } ,
156+ ) )
157+ }
158+
159+ fn new_basic_with_passcode (
160+ username : String ,
161+ tenant : Option < String > ,
162+ protected : bool ,
163+ PassCode { password, hash } : PassCode ,
164+ ) -> ( Self , String ) {
110165 (
111166 Self {
112167 ty : UserType :: Native ( Basic {
@@ -705,6 +760,49 @@ impl UserGroup {
705760 }
706761}
707762
763+ #[ cfg( test) ]
764+ mod provided_password_tests {
765+ use super :: * ;
766+
767+ #[ test]
768+ fn accepts_legacy_roles_array_and_new_password_object ( ) {
769+ let legacy: CreateUserRequest = serde_json:: from_str ( r#"["reader"]"# ) . unwrap ( ) ;
770+ let ( roles, password) = legacy. split_request ( ) ;
771+ assert ! ( roles. contains( "reader" ) ) ;
772+ assert ! ( password. is_none( ) ) ;
773+
774+ let modern: CreateUserRequest = serde_json:: from_str (
775+ r#"{"roles":["reader"],"providedPassword":"a_valid_password_123"}"# ,
776+ )
777+ . unwrap ( ) ;
778+ let ( roles, password) = modern. split_request ( ) ;
779+ assert ! ( roles. contains( "reader" ) ) ;
780+ assert_eq ! ( password. as_deref( ) , Some ( "a_valid_password_123" ) ) ;
781+
782+ assert ! (
783+ serde_json:: from_str:: <CreateUserRequest >( r#"{"providedPasswrod":"typo"}"# ) . is_err( )
784+ ) ;
785+ }
786+
787+ #[ test]
788+ fn supplied_password_is_hashed_and_invalid_values_are_rejected ( ) {
789+ let ( user, returned) =
790+ User :: new_basic_with_provided_password ( "alice" . into ( ) , None , false , "Pa$$w0rd" . into ( ) )
791+ . unwrap ( ) ;
792+ assert_eq ! ( returned, "Pa$$w0rd" ) ;
793+ let UserType :: Native ( basic) = & user. ty else {
794+ panic ! ( "expected native user" )
795+ } ;
796+ assert ! ( basic. verify_password( & returned) ) ;
797+ assert ! ( !serde_json:: to_string( & user) . unwrap( ) . contains( & returned) ) ;
798+
799+ assert ! ( matches!(
800+ User :: new_basic_with_provided_password( "bob" . into( ) , None , false , "short" . into( ) ) ,
801+ Err ( RBACError :: InvalidProvidedPassword )
802+ ) ) ;
803+ }
804+ }
805+
708806#[ cfg( test) ]
709807mod apikey_serde_tests {
710808 use super :: * ;
0 commit comments