Skip to content

Allow spaces in user names and auto-generate login - #1158

Merged
carma12 merged 1 commit into
freeipa:mainfrom
carma12:last-name-with-spaces
Aug 14, 2026
Merged

Allow spaces in user names and auto-generate login#1158
carma12 merged 1 commit into
freeipa:mainfrom
carma12:last-name-with-spaces

Conversation

@carma12

@carma12 carma12 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

The Add User modal previously rejected first/last names containing spaces (e.g., "Maria José", "Van den Berg"), which are valid values for the givenname/sn LDAP attributes in FreeIPA.

This change:

  • Removes the whitespace restriction from name field validation, allowing composed names and multi-word surnames.
  • Auto-generates a sanitized user login (uid) from the first and last name as the user types, stripping diacritics, spaces, and other characters invalid for a login. This prevents the server-side ValidationError that occurred when no uid was provided and the auto-generated default contained spaces.
  • Allows the user to override the suggested login; clearing it re-triggers auto-generation from the current name values.

Assisted-by: Claude noreply@anthropic.com
Fixes: #1151

Summary by Sourcery

Allow spaces in user first and last names in the Add User modal and auto-generate a sanitized login from those names, while still permitting manual override.

New Features:

  • Automatically derive a suggested user login from the entered first and last names, sanitizing it to match FreeIPA uid constraints and updating as the user types.

Enhancements:

  • Relax first and last name validation to accept spaces while still blocking special characters.
  • Track whether the login field has been manually edited to stop auto-generation and resume it if the field is cleared.
  • Reset the login touch state when clearing the Add User form to restore default auto-generation behavior.

@carma12 carma12 self-assigned this Aug 12, 2026
@carma12 carma12 added the needs-review This PR is waiting on a review label Aug 12, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/components/modals/UserModals/AddUser.tsx" line_range="68" />
<code_context>

   // useStates for TextInputs
   const [userLogin, setUserLogin] = React.useState("");
+  const [loginTouched, setLoginTouched] = React.useState(false);
   const [firstName, setFirstName] = React.useState("");
   const [lastName, setLastName] = React.useState("");
</code_context>
<issue_to_address>
**issue (complexity):** Consider replacing the loginTouched-based handler logic with a single hasCustomLogin flag, a useEffect-driven login generator, and extracted helper functions to simplify state and branching in this modal.

Using `loginTouched` plus three handlers does add avoidable complexity and splits the generation logic across multiple branches.

You can keep all behavior and simplify by:

1. **Use a single `hasCustomLogin` flag and centralize generation in `useEffect`.**
2. **Extract `generateLogin`/`sanitize` out of the component.**

### 1. Centralize login generation

Instead of recomputing login in each handler, track whether the user has overridden the login and recompute in one place:

```ts
// state
const [userLogin, setUserLogin] = React.useState("");
const [hasCustomLogin, setHasCustomLogin] = React.useState(false);
const [firstName, setFirstName] = React.useState("");
const [lastName, setLastName] = React.useState("");

// effect: auto-generate login from latest names when not customized
useEffect(() => {
  if (!hasCustomLogin) {
    setUserLogin(generateLogin(firstName, lastName));
  }
}, [firstName, lastName, hasCustomLogin]);
```

Handlers then become straight updates with a single place for the generation logic:

```tsx
const handleFirstNameChange = (value: string) => {
  setFirstName(value);
};

const handleLastNameChange = (value: string) => {
  setLastName(value);
};

const handleLoginChange = (value: string) => {
  setHasCustomLogin(value.length > 0);
  setUserLogin(value);
};
```

This preserves:
- auto-generation when first/last change,
- user override when they type in login,
- clearing login (set `hasCustomLogin` false, effect re-applies generated login).

### 2. Extract sanitization/generation helpers

Move sanitization outside the component to reduce UI noise and keep behavior the same:

```ts
// utils/login.ts
export const sanitizeLoginPart = (s: string): string =>
  s
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^A-Za-z0-9._\-$]/g, "")
    .toLowerCase();

export const generateLogin = (first: string, last: string): string => {
  const sanitizedFirst = sanitizeLoginPart(first);
  const sanitizedLast = sanitizeLoginPart(last);

  if (!sanitizedFirst || !sanitizedLast) return "";
  return sanitizedFirst.charAt(0) + sanitizedLast;
};
```

Then in the component:

```ts
import { generateLogin } from "./utils/login";
```

This keeps all functionality intact but reduces branching and “mental load” in the modal component.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/components/modals/UserModals/AddUser.tsx Outdated
@carma12
carma12 force-pushed the last-name-with-spaces branch from 3ae8d8d to c5c5c3b Compare August 12, 2026 14:03

@duzda duzda left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hi, please see notes, we should remove the unnecessary rules, honestly in this day and age, I wouldn't be surprised to see anyone with a ? in their name.

The API returns this JSON:

        "takes_args": [
          {
            "cli_name": "login",
            "deprecated_cli_aliases": [],
            "label": "User login",
            "doc": "User login",
            "required": true,
            "multivalue": false,
            "primary_key": true,
            "autofill": false,
            "query": false,
            "attribute": true,
            "flags": [],
            "alwaysask": false,
            "sortorder": 2,
            "cli_metavar": "STR",
            "no_convert": false,
            "deprecated": false,
            "confirm": true,
            "maxlength": 255,
            "pattern_errmsg": "may only include letters, numbers, _, -, . and $, refer to 'ipa help user' for complete format description",
            "pattern": "(?!^[0-9]+$)^[a-zA-Z0-9_.][a-zA-Z0-9_.-]*[a-zA-Z0-9_.$-]?$",
            "noextrawhitespace": true,
            "class": "Str",
            "name": "uid",
            "type": "str"
          }
        ],
        "takes_options": [
          {
            "cli_name": "first",
            "deprecated_cli_aliases": [],
            "label": "First name",
            "doc": "First name",
            "required": true,
            "multivalue": false,
            "primary_key": false,
            "autofill": false,
            "query": false,
            "attribute": true,
            "flags": [],
            "alwaysask": false,
            "sortorder": 2,
            "cli_metavar": "STR",
            "no_convert": false,
            "deprecated": false,
            "confirm": true,
            "noextrawhitespace": true,
            "class": "Str",
            "name": "givenname",
            "type": "str"
          },
          {
            "cli_name": "last",
            "deprecated_cli_aliases": [],
            "label": "Last name",
            "doc": "Last name",
            "required": true,
            "multivalue": false,
            "primary_key": false,
            "autofill": false,
            "query": false,
            "attribute": true,
            "flags": [],
            "alwaysask": false,
            "sortorder": 2,
            "cli_metavar": "STR",
            "no_convert": false,
            "deprecated": false,
            "confirm": true,
            "noextrawhitespace": true,
            "class": "Str",
            "name": "sn",
            "type": "str"
          },

No mention of any pattern for first and last name, the only pattern is for userLogin, which should be changed.

Comment thread src/utils/loginUtils.ts
* Generates a sanitized login from first + last name,
* matching FreeIPA's default: givenname[0] + sn (lowercased).
*/
export const generateLogin = (first: string, last: string): string => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's no need for this code, please don't generate login on the frontend, it will be handled by backend if you omit it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The issue is that the server's auto-generation (givenname[0] + sn) does not sanitize, it just concatenates as-is. So with sn="Rodriguez Perez", the server would try to create login mrodriguez perez, which then fails its own pattern validation because spaces aren't allowed in a uid.

Another example:

>> ipa user-add
First name: Anselmo
Last name: Luján Espósito
User login [aluján espósito]: 
ipa: ERROR: invalid 'login': may only include letters, numbers, _, -, . and $, refer to 'ipa help user' for complete format description

The generateLogin function aims to fix that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you please raise an issue on the backend then?

Comment on lines 212 to 220
rules={[
{
id: "ruleCharacters",
message:
"First name should not contain special characters or spaces",
validate: (v: string) => !formatWithoutSpaces.test(v),
message: "First name should not contain special characters",
validate: (v: string) => !nameInvalidChars.test(v),
},
]}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please delete this code, all of it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I might need more context to understand why is this wrong.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There is no pattern or first and last name, you can put anything you want there on the backend, we should follow that, I'm confident that people may have weird names, or use a placeholder.

Comment on lines 235 to 243
rules={[
{
id: "ruleCharacters",
message:
"Last name should not contain special characters or spaces",
validate: (v: string) => !formatWithoutSpaces.test(v),
message: "Last name should not contain special characters",
validate: (v: string) => !nameInvalidChars.test(v),
},
]}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please delete this code, all of it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Same.

@@ -87,7 +95,7 @@ const AddUser = (props: PropsToAddUser) => {
// User login: Valid characters in body (every char must be in set): letters, digits, '_', '-', '.', '$'
const userLoginFormatBody = /^[A-Za-z0-9._\-$]*$/;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The only pattern is for userLogin and it's this one: "(?!^[0-9]+$)^[a-zA-Z0-9_.][a-zA-Z0-9_.-]*[a-zA-Z0-9_.$-]?$"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

And it is being applied.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Just read your main comment. Maybe I can replace this by the pattern provided in the metadata...

const [addSpinning, setAddBtnSpinning] = React.useState<boolean>(false);

// Login is either the user-provided value or auto-generated from names
const userLogin = customLogin || generateLogin(firstName, lastName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe this change is not needed?

@carma12 carma12 Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It is needed. This is because there is two ways to actually have a user name in the UI: either manually providing it itself (customLogin), or generated (generateLogin).

If we leave that field empty, 1) the ipa user-add command complains, because it is not able to generate a user name based on the data provided, and 2) it is possible from the UI to just "delete" the provided user name (generated or not), causing the problem mentioned here.

This is a compromise solution to ensure the user doesn't mess up with the UI.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It would be nicer to put the generated login as placeholder into the input, instead of "typing" for the user.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That sounds reasonable, will do...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done.

@carma12
carma12 force-pushed the last-name-with-spaces branch from c5c5c3b to 0c8d83a Compare August 13, 2026 13:40
@carma12

carma12 commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Hi, please see notes, we should remove the unnecessary rules, honestly in this day and age, I wouldn't be surprised to see anyone with a ? in their name.

The API returns this JSON:

        "takes_args": [
          {
            "cli_name": "login",
            "deprecated_cli_aliases": [],
            "label": "User login",
            "doc": "User login",
            "required": true,
            "multivalue": false,
            "primary_key": true,
            "autofill": false,
            "query": false,
            "attribute": true,
            "flags": [],
            "alwaysask": false,
            "sortorder": 2,
            "cli_metavar": "STR",
            "no_convert": false,
            "deprecated": false,
            "confirm": true,
            "maxlength": 255,
            "pattern_errmsg": "may only include letters, numbers, _, -, . and $, refer to 'ipa help user' for complete format description",
            "pattern": "(?!^[0-9]+$)^[a-zA-Z0-9_.][a-zA-Z0-9_.-]*[a-zA-Z0-9_.$-]?$",
            "noextrawhitespace": true,
            "class": "Str",
            "name": "uid",
            "type": "str"
          }
        ],
        "takes_options": [
          {
            "cli_name": "first",
            "deprecated_cli_aliases": [],
            "label": "First name",
            "doc": "First name",
            "required": true,
            "multivalue": false,
            "primary_key": false,
            "autofill": false,
            "query": false,
            "attribute": true,
            "flags": [],
            "alwaysask": false,
            "sortorder": 2,
            "cli_metavar": "STR",
            "no_convert": false,
            "deprecated": false,
            "confirm": true,
            "noextrawhitespace": true,
            "class": "Str",
            "name": "givenname",
            "type": "str"
          },
          {
            "cli_name": "last",
            "deprecated_cli_aliases": [],
            "label": "Last name",
            "doc": "Last name",
            "required": true,
            "multivalue": false,
            "primary_key": false,
            "autofill": false,
            "query": false,
            "attribute": true,
            "flags": [],
            "alwaysask": false,
            "sortorder": 2,
            "cli_metavar": "STR",
            "no_convert": false,
            "deprecated": false,
            "confirm": true,
            "noextrawhitespace": true,
            "class": "Str",
            "name": "sn",
            "type": "str"
          },

No mention of any pattern for first and last name, the only pattern is for userLogin, which should be changed.

I have changed the code to take the defined pattern for the user name.

@carma12
carma12 force-pushed the last-name-with-spaces branch 2 times, most recently from 7b9ec97 to a74e723 Compare August 13, 2026 14:53
@duzda

duzda commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Hi, you still need to remove the patterns for first and last name, there are no patterns on the backend, let's follow that.

The Add User modal previously rejected first/last names containing
spaces (e.g., "Maria José", "Van den Berg"), which are valid values
for the givenname/sn LDAP attributes in FreeIPA.

This change:
- Removes the whitespace restriction from name field validation,
  allowing composed names and multi-word surnames.
- Auto-generates a sanitized user login (uid) from the first and last
  name as the user types, stripping diacritics, spaces, and other
  characters invalid for a login. This prevents the server-side
  ValidationError that occurred when no uid was provided and the
  auto-generated default contained spaces.
- Allows the user to override the suggested login; clearing it
  re-triggers auto-generation from the current name values.

Assisted-by: Claude <noreply@anthropic.com>
Fixes: freeipa#1151
Signed-off-by: Carla Martinez <carlmart@redhat.com>
@carma12
carma12 force-pushed the last-name-with-spaces branch from a74e723 to 4a9a0ca Compare August 14, 2026 07:32

@veronnicka veronnicka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, maybe mark somewhere that the generate login code is a temporary fix until the issue is fixed on the backend..

@carma12
carma12 merged commit d67c221 into freeipa:main Aug 14, 2026
7 of 10 checks passed
@carma12

carma12 commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

LGTM, maybe mark somewhere that the generate login code is a temporary fix until the issue is fixed on the backend..

Just read your comment after merging the PR 🙈 But I can add it afterwards in any other PR 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review This PR is waiting on a review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unable to create users with spaces in the last name field

3 participants