feat: adds projectID prop in IAM user account

Closes #1621

These changes introduce the `projectID` field in IAM user accounts. The field has been added across all IAM systems: internal, IPA, LDAP, Vault, and S3 object. Support has also been added to the admin CLI commands to create, update, and list users with the `projectID` included.
This commit is contained in:
niksis02
2025-11-07 20:33:14 +04:00
parent 743cb03808
commit a64733bfbe
8 changed files with 125 additions and 53 deletions

View File

@@ -45,11 +45,12 @@ func (r Role) IsValid() bool {
// Account is a gateway IAM account // Account is a gateway IAM account
type Account struct { type Account struct {
Access string `json:"access"` Access string `json:"access"`
Secret string `json:"secret"` Secret string `json:"secret"`
Role Role `json:"role"` Role Role `json:"role"`
UserID int `json:"userID"` UserID int `json:"userID"`
GroupID int `json:"groupID"` GroupID int `json:"groupID"`
ProjectID int `json:"projectID"`
} }
type ListUserAccountsResult struct { type ListUserAccountsResult struct {
@@ -58,10 +59,11 @@ type ListUserAccountsResult struct {
// Mutable props, which could be changed when updating an IAM account // Mutable props, which could be changed when updating an IAM account
type MutableProps struct { type MutableProps struct {
Secret *string `json:"secret"` Secret *string `json:"secret"`
Role Role `json:"role"` Role Role `json:"role"`
UserID *int `json:"userID"` UserID *int `json:"userID"`
GroupID *int `json:"groupID"` GroupID *int `json:"groupID"`
ProjectID *int `json:"projectID"`
} }
func (m MutableProps) Validate() error { func (m MutableProps) Validate() error {
@@ -82,6 +84,9 @@ func updateAcc(acc *Account, props MutableProps) {
if props.UserID != nil { if props.UserID != nil {
acc.UserID = *props.UserID acc.UserID = *props.UserID
} }
if props.ProjectID != nil {
acc.ProjectID = *props.ProjectID
}
if props.Role != "" { if props.Role != "" {
acc.Role = props.Role acc.Role = props.Role
} }
@@ -119,6 +124,7 @@ type Opts struct {
LDAPRoleAtr string LDAPRoleAtr string
LDAPUserIdAtr string LDAPUserIdAtr string
LDAPGroupIdAtr string LDAPGroupIdAtr string
LDAPProjectIdAtr string
LDAPTLSSkipVerify bool LDAPTLSSkipVerify bool
VaultEndpointURL string VaultEndpointURL string
VaultNamespace string VaultNamespace string
@@ -160,7 +166,7 @@ func New(o *Opts) (IAMService, error) {
case o.LDAPServerURL != "": case o.LDAPServerURL != "":
svc, err = NewLDAPService(o.RootAccount, o.LDAPServerURL, o.LDAPBindDN, o.LDAPPassword, svc, err = NewLDAPService(o.RootAccount, o.LDAPServerURL, o.LDAPBindDN, o.LDAPPassword,
o.LDAPQueryBase, o.LDAPAccessAtr, o.LDAPSecretAtr, o.LDAPRoleAtr, o.LDAPUserIdAtr, o.LDAPQueryBase, o.LDAPAccessAtr, o.LDAPSecretAtr, o.LDAPRoleAtr, o.LDAPUserIdAtr,
o.LDAPGroupIdAtr, o.LDAPObjClasses, o.LDAPTLSSkipVerify) o.LDAPGroupIdAtr, o.LDAPProjectIdAtr, o.LDAPObjClasses, o.LDAPTLSSkipVerify)
fmt.Printf("initializing LDAP IAM with %q\n", o.LDAPServerURL) fmt.Printf("initializing LDAP IAM with %q\n", o.LDAPServerURL)
case o.S3Endpoint != "": case o.S3Endpoint != "":
svc, err = NewS3(o.RootAccount, o.S3Access, o.S3Secret, o.S3Region, o.S3Bucket, svc, err = NewS3(o.RootAccount, o.S3Access, o.S3Secret, o.S3Region, o.S3Bucket,

View File

@@ -194,11 +194,12 @@ func (s *IAMServiceInternal) ListUserAccounts() ([]Account, error) {
var accs []Account var accs []Account
for _, k := range keys { for _, k := range keys {
accs = append(accs, Account{ accs = append(accs, Account{
Access: k, Access: k,
Secret: conf.AccessAccounts[k].Secret, Secret: conf.AccessAccounts[k].Secret,
Role: conf.AccessAccounts[k].Role, Role: conf.AccessAccounts[k].Role,
UserID: conf.AccessAccounts[k].UserID, UserID: conf.AccessAccounts[k].UserID,
GroupID: conf.AccessAccounts[k].GroupID, GroupID: conf.AccessAccounts[k].GroupID,
ProjectID: conf.AccessAccounts[k].ProjectID,
}) })
} }

View File

@@ -132,6 +132,7 @@ func (ipa *IpaIAMService) GetUserAccount(access string) (Account, error) {
userResult := struct { userResult := struct {
Gidnumber []string Gidnumber []string
Uidnumber []string Uidnumber []string
PidNumber []string
}{} }{}
err = ipa.rpc(req, &userResult) err = ipa.rpc(req, &userResult)
@@ -147,12 +148,17 @@ func (ipa *IpaIAMService) GetUserAccount(access string) (Account, error) {
if err != nil { if err != nil {
return Account{}, fmt.Errorf("ipa gid invalid: %w", err) return Account{}, fmt.Errorf("ipa gid invalid: %w", err)
} }
pId, err := strconv.Atoi(userResult.PidNumber[0])
if err != nil {
return Account{}, fmt.Errorf("ipa pid invalid: %w", err)
}
account := Account{ account := Account{
Access: access, Access: access,
Role: RoleUser, Role: RoleUser,
UserID: uid, UserID: uid,
GroupID: gid, GroupID: gid,
ProjectID: pId,
} }
session_key := make([]byte, 16) session_key := make([]byte, 16)

View File

@@ -36,6 +36,7 @@ type LdapIAMService struct {
roleAtr string roleAtr string
groupIdAtr string groupIdAtr string
userIdAtr string userIdAtr string
projectIdAtr string
rootAcc Account rootAcc Account
url string url string
bindDN string bindDN string
@@ -46,9 +47,9 @@ type LdapIAMService struct {
var _ IAMService = &LdapIAMService{} var _ IAMService = &LdapIAMService{}
func NewLDAPService(rootAcc Account, ldapURL, bindDN, pass, queryBase, accAtr, secAtr, roleAtr, userIdAtr, groupIdAtr, objClasses string, tlsSkipVerify bool) (IAMService, error) { func NewLDAPService(rootAcc Account, ldapURL, bindDN, pass, queryBase, accAtr, secAtr, roleAtr, userIdAtr, groupIdAtr, projectIdAtr, objClasses string, tlsSkipVerify bool) (IAMService, error) {
if ldapURL == "" || bindDN == "" || pass == "" || queryBase == "" || accAtr == "" || if ldapURL == "" || bindDN == "" || pass == "" || queryBase == "" || accAtr == "" ||
secAtr == "" || roleAtr == "" || userIdAtr == "" || groupIdAtr == "" || objClasses == "" { secAtr == "" || roleAtr == "" || userIdAtr == "" || groupIdAtr == "" || projectIdAtr == "" || objClasses == "" {
return nil, fmt.Errorf("required parameters list not fully provided") return nil, fmt.Errorf("required parameters list not fully provided")
} }
@@ -71,6 +72,7 @@ func NewLDAPService(rootAcc Account, ldapURL, bindDN, pass, queryBase, accAtr, s
roleAtr: roleAtr, roleAtr: roleAtr,
userIdAtr: userIdAtr, userIdAtr: userIdAtr,
groupIdAtr: groupIdAtr, groupIdAtr: groupIdAtr,
projectIdAtr: projectIdAtr,
rootAcc: rootAcc, rootAcc: rootAcc,
url: ldapURL, url: ldapURL,
bindDN: bindDN, bindDN: bindDN,
@@ -142,6 +144,7 @@ func (ld *LdapIAMService) CreateAccount(account Account) error {
userEntry.Attribute(ld.roleAtr, []string{string(account.Role)}) userEntry.Attribute(ld.roleAtr, []string{string(account.Role)})
userEntry.Attribute(ld.groupIdAtr, []string{fmt.Sprint(account.GroupID)}) userEntry.Attribute(ld.groupIdAtr, []string{fmt.Sprint(account.GroupID)})
userEntry.Attribute(ld.userIdAtr, []string{fmt.Sprint(account.UserID)}) userEntry.Attribute(ld.userIdAtr, []string{fmt.Sprint(account.UserID)})
userEntry.Attribute(ld.projectIdAtr, []string{fmt.Sprint(account.ProjectID)})
err := ld.execute(func(c *ldap.Conn) error { err := ld.execute(func(c *ldap.Conn) error {
return c.Add(userEntry) return c.Add(userEntry)
@@ -177,7 +180,7 @@ func (ld *LdapIAMService) GetUserAccount(access string) (Account, error) {
0, 0,
false, false,
ld.buildSearchFilter(access), ld.buildSearchFilter(access),
[]string{ld.accessAtr, ld.secretAtr, ld.roleAtr, ld.userIdAtr, ld.groupIdAtr}, []string{ld.accessAtr, ld.secretAtr, ld.roleAtr, ld.userIdAtr, ld.groupIdAtr, ld.projectIdAtr},
nil, nil,
) )
@@ -216,12 +219,19 @@ func (ld *LdapIAMService) GetUserAccount(access string) (Account, error) {
return Account{}, fmt.Errorf("invalid entry value for user-id %q: %w", return Account{}, fmt.Errorf("invalid entry value for user-id %q: %w",
entry.GetAttributeValue(ld.userIdAtr), err) entry.GetAttributeValue(ld.userIdAtr), err)
} }
projectID, err := strconv.Atoi(entry.GetAttributeValue(ld.projectIdAtr))
if err != nil {
return Account{}, fmt.Errorf("invalid entry value for project-id %q: %w",
entry.GetAttributeValue(ld.projectIdAtr), err)
}
return Account{ return Account{
Access: entry.GetAttributeValue(ld.accessAtr), Access: entry.GetAttributeValue(ld.accessAtr),
Secret: entry.GetAttributeValue(ld.secretAtr), Secret: entry.GetAttributeValue(ld.secretAtr),
Role: Role(entry.GetAttributeValue(ld.roleAtr)), Role: Role(entry.GetAttributeValue(ld.roleAtr)),
GroupID: groupId, GroupID: groupId,
UserID: userId, UserID: userId,
ProjectID: projectID,
}, nil }, nil
} }
@@ -236,6 +246,9 @@ func (ld *LdapIAMService) UpdateUserAccount(access string, props MutableProps) e
if props.UserID != nil { if props.UserID != nil {
req.Replace(ld.userIdAtr, []string{fmt.Sprint(*props.UserID)}) req.Replace(ld.userIdAtr, []string{fmt.Sprint(*props.UserID)})
} }
if props.ProjectID != nil {
req.Replace(ld.projectIdAtr, []string{fmt.Sprint(*props.ProjectID)})
}
if props.Role != "" { if props.Role != "" {
req.Replace(ld.roleAtr, []string{string(props.Role)}) req.Replace(ld.roleAtr, []string{string(props.Role)})
} }
@@ -273,7 +286,7 @@ func (ld *LdapIAMService) ListUserAccounts() ([]Account, error) {
0, 0,
false, false,
ld.buildSearchFilter(""), ld.buildSearchFilter(""),
[]string{ld.accessAtr, ld.secretAtr, ld.roleAtr, ld.groupIdAtr, ld.userIdAtr}, []string{ld.accessAtr, ld.secretAtr, ld.roleAtr, ld.groupIdAtr, ld.projectIdAtr, ld.userIdAtr},
nil, nil,
) )
@@ -298,12 +311,19 @@ func (ld *LdapIAMService) ListUserAccounts() ([]Account, error) {
return nil, fmt.Errorf("invalid entry value for user-id %q: %w", return nil, fmt.Errorf("invalid entry value for user-id %q: %w",
el.GetAttributeValue(ld.userIdAtr), err) el.GetAttributeValue(ld.userIdAtr), err)
} }
projectID, err := strconv.Atoi(el.GetAttributeValue(ld.projectIdAtr))
if err != nil {
return nil, fmt.Errorf("invalid entry value for project-id %q: %w",
el.GetAttributeValue(ld.groupIdAtr), err)
}
result = append(result, Account{ result = append(result, Account{
Access: el.GetAttributeValue(ld.accessAtr), Access: el.GetAttributeValue(ld.accessAtr),
Secret: el.GetAttributeValue(ld.secretAtr), Secret: el.GetAttributeValue(ld.secretAtr),
Role: Role(el.GetAttributeValue(ld.roleAtr)), Role: Role(el.GetAttributeValue(ld.roleAtr)),
GroupID: groupId, GroupID: groupId,
UserID: userId, ProjectID: projectID,
UserID: userId,
}) })
} }

View File

@@ -205,11 +205,12 @@ func (s *IAMServiceS3) ListUserAccounts() ([]Account, error) {
var accs []Account var accs []Account
for _, k := range keys { for _, k := range keys {
accs = append(accs, Account{ accs = append(accs, Account{
Access: k, Access: k,
Secret: conf.AccessAccounts[k].Secret, Secret: conf.AccessAccounts[k].Secret,
Role: conf.AccessAccounts[k].Role, Role: conf.AccessAccounts[k].Role,
UserID: conf.AccessAccounts[k].UserID, UserID: conf.AccessAccounts[k].UserID,
GroupID: conf.AccessAccounts[k].GroupID, GroupID: conf.AccessAccounts[k].GroupID,
ProjectID: conf.AccessAccounts[k].ProjectID,
}) })
} }

View File

@@ -369,12 +369,21 @@ func parseVaultUserAccount(data map[string]any, access string) (acc Account, err
if err != nil { if err != nil {
return acc, errInvalidUser return acc, errInvalidUser
} }
projectIdJson, ok := usrAcc["projectID"].(json.Number)
if !ok {
return acc, errInvalidUser
}
projectID, err := projectIdJson.Int64()
if err != nil {
return acc, errInvalidUser
}
return Account{ return Account{
Access: acss, Access: acss,
Secret: secret, Secret: secret,
Role: Role(role), Role: Role(role),
UserID: int(userId), UserID: int(userId),
GroupID: int(groupId), GroupID: int(groupId),
ProjectID: int(projectID),
}, nil }, nil
} }

View File

@@ -82,6 +82,11 @@ func adminCommand() *cli.Command {
Usage: "groupID for the new user", Usage: "groupID for the new user",
Aliases: []string{"gi"}, Aliases: []string{"gi"},
}, },
&cli.IntFlag{
Name: "project-id",
Usage: "projectID for the new user",
Aliases: []string{"pi"},
},
}, },
}, },
{ {
@@ -115,6 +120,11 @@ func adminCommand() *cli.Command {
Usage: "groupID for the new user", Usage: "groupID for the new user",
Aliases: []string{"gi"}, Aliases: []string{"gi"},
}, },
&cli.IntFlag{
Name: "project-id",
Usage: "projectID for the new user",
Aliases: []string{"pi"},
},
}, },
}, },
{ {
@@ -214,7 +224,7 @@ func initHTTPClient() *http.Client {
func createUser(ctx *cli.Context) error { func createUser(ctx *cli.Context) error {
access, secret, role := ctx.String("access"), ctx.String("secret"), ctx.String("role") access, secret, role := ctx.String("access"), ctx.String("secret"), ctx.String("role")
userID, groupID := ctx.Int("user-id"), ctx.Int("group-id") userID, groupID, projectID := ctx.Int("user-id"), ctx.Int("group-id"), ctx.Int("project-id")
if access == "" || secret == "" { if access == "" || secret == "" {
return fmt.Errorf("invalid input parameters for the new user access/secret keys") return fmt.Errorf("invalid input parameters for the new user access/secret keys")
} }
@@ -223,11 +233,12 @@ func createUser(ctx *cli.Context) error {
} }
acc := auth.Account{ acc := auth.Account{
Access: access, Access: access,
Secret: secret, Secret: secret,
Role: auth.Role(role), Role: auth.Role(role),
UserID: userID, UserID: userID,
GroupID: groupID, GroupID: groupID,
ProjectID: projectID,
} }
accxml, err := xml.Marshal(acc) accxml, err := xml.Marshal(acc)
@@ -316,7 +327,14 @@ func deleteUser(ctx *cli.Context) error {
} }
func updateUser(ctx *cli.Context) error { func updateUser(ctx *cli.Context) error {
access, secret, userId, groupId, role := ctx.String("access"), ctx.String("secret"), ctx.Int("user-id"), ctx.Int("group-id"), auth.Role(ctx.String("role")) access, secret, userId, groupId, projectID, role :=
ctx.String("access"),
ctx.String("secret"),
ctx.Int("user-id"),
ctx.Int("group-id"),
ctx.Int("projectID"),
auth.Role(ctx.String("role"))
props := auth.MutableProps{} props := auth.MutableProps{}
if ctx.IsSet("role") { if ctx.IsSet("role") {
if !role.IsValid() { if !role.IsValid() {
@@ -333,6 +351,9 @@ func updateUser(ctx *cli.Context) error {
if ctx.IsSet("group-id") { if ctx.IsSet("group-id") {
props.GroupID = &groupId props.GroupID = &groupId
} }
if ctx.IsSet("project-id") {
props.ProjectID = &projectID
}
propsxml, err := xml.Marshal(props) propsxml, err := xml.Marshal(props)
if err != nil { if err != nil {
@@ -433,10 +454,10 @@ const (
func printAcctTable(accs []auth.Account) { func printAcctTable(accs []auth.Account) {
w := new(tabwriter.Writer) w := new(tabwriter.Writer)
w.Init(os.Stdout, minwidth, tabwidth, padding, padchar, flags) w.Init(os.Stdout, minwidth, tabwidth, padding, padchar, flags)
fmt.Fprintln(w, "Account\tRole\tUserID\tGroupID") fmt.Fprintln(w, "Account\tRole\tUserID\tGroupID\tProjectID")
fmt.Fprintln(w, "-------\t----\t------\t-------") fmt.Fprintln(w, "-------\t----\t------\t-------\t---------")
for _, acc := range accs { for _, acc := range accs {
fmt.Fprintf(w, "%v\t%v\t%v\t%v\n", acc.Access, acc.Role, acc.UserID, acc.GroupID) fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\n", acc.Access, acc.Role, acc.UserID, acc.GroupID, acc.ProjectID)
} }
fmt.Fprintln(w) fmt.Fprintln(w)
w.Flush() w.Flush()

View File

@@ -65,6 +65,7 @@ var (
ldapQueryBase, ldapObjClasses string ldapQueryBase, ldapObjClasses string
ldapAccessAtr, ldapSecAtr, ldapRoleAtr string ldapAccessAtr, ldapSecAtr, ldapRoleAtr string
ldapUserIdAtr, ldapGroupIdAtr string ldapUserIdAtr, ldapGroupIdAtr string
ldapProjectIdAtr string
ldapTLSSkipVerify bool ldapTLSSkipVerify bool
vaultEndpointURL, vaultNamespace string vaultEndpointURL, vaultNamespace string
vaultSecretStoragePath string vaultSecretStoragePath string
@@ -405,6 +406,12 @@ func initFlags() []cli.Flag {
EnvVars: []string{"VGW_IAM_LDAP_GROUP_ID_ATR"}, EnvVars: []string{"VGW_IAM_LDAP_GROUP_ID_ATR"},
Destination: &ldapGroupIdAtr, Destination: &ldapGroupIdAtr,
}, },
&cli.StringFlag{
Name: "iam-ldap-project-id-atr",
Usage: "ldap server user project id attribute name",
EnvVars: []string{"VGW_IAM_LDAP_PROJECT_ID_ATR"},
Destination: &ldapProjectIdAtr,
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "iam-ldap-tls-skip-verify", Name: "iam-ldap-tls-skip-verify",
Usage: "disable TLS certificate verification for LDAP connections (insecure, for self-signed certificates)", Usage: "disable TLS certificate verification for LDAP connections (insecure, for self-signed certificates)",
@@ -699,6 +706,7 @@ func runGateway(ctx context.Context, be backend.Backend) error {
LDAPRoleAtr: ldapRoleAtr, LDAPRoleAtr: ldapRoleAtr,
LDAPUserIdAtr: ldapUserIdAtr, LDAPUserIdAtr: ldapUserIdAtr,
LDAPGroupIdAtr: ldapGroupIdAtr, LDAPGroupIdAtr: ldapGroupIdAtr,
LDAPProjectIdAtr: ldapProjectIdAtr,
LDAPTLSSkipVerify: ldapTLSSkipVerify, LDAPTLSSkipVerify: ldapTLSSkipVerify,
VaultEndpointURL: vaultEndpointURL, VaultEndpointURL: vaultEndpointURL,
VaultNamespace: vaultNamespace, VaultNamespace: vaultNamespace,