From 73003068864aba876bf4a1be00a6e8a15f1c587d Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Fri, 29 May 2026 20:53:06 -0700 Subject: [PATCH] feat: add group-based role mapping for FreeIPA IAM FreeIPA users were always assigned RoleUser regardless of their group membership. This adds --ipa-admin-group-cn so that members of a specified FreeIPA group are automatically granted the admin role, and --ipa-enable-userplus to elevate the default role to userplus for all IPA users. The user_show RPC is updated to request all attributes so that memberof_group is reliably returned. --- auth/iam.go | 4 +++- auth/iam_ipa.go | 39 ++++++++++++++++++++++++++------------- cmd/versitygw/main.go | 17 ++++++++++++++++- embedgw/embedgw.go | 8 ++++++++ extra/example.conf | 7 +++++++ 5 files changed, 60 insertions(+), 15 deletions(-) diff --git a/auth/iam.go b/auth/iam.go index 13f47517..c939e993 100644 --- a/auth/iam.go +++ b/auth/iam.go @@ -152,6 +152,8 @@ type Opts struct { IpaVaultName string IpaUser string IpaPassword string + IpaAdminGroupCN string + IpaEnableUserPlus bool IpaInsecure bool } @@ -179,7 +181,7 @@ func New(o *Opts) (IAMService, error) { o.VaultServerCert, o.VaultClientCert, o.VaultClientCertKey) fmt.Printf("initializing Vault IAM with %q\n", o.VaultEndpointURL) case o.IpaHost != "": - svc, err = NewIpaIAMService(o.RootAccount, o.IpaHost, o.IpaVaultName, o.IpaUser, o.IpaPassword, o.IpaInsecure) + svc, err = NewIpaIAMService(o.RootAccount, o.IpaHost, o.IpaVaultName, o.IpaUser, o.IpaPassword, o.IpaAdminGroupCN, o.IpaEnableUserPlus, o.IpaInsecure) fmt.Printf("initializing IPA IAM with %q\n", o.IpaHost) default: // if no iam options selected, default to the single user mode diff --git a/auth/iam_ipa.go b/auth/iam_ipa.go index e54e2e03..65247b98 100644 --- a/auth/iam_ipa.go +++ b/auth/iam_ipa.go @@ -49,21 +49,25 @@ type IpaIAMService struct { vaultName string username string password string + adminGroupCN string + enableUserPlus bool kraTransportKey *rsa.PublicKey rootAcc Account } var _ IAMService = &IpaIAMService{} -func NewIpaIAMService(rootAcc Account, host, vaultName, username, password string, isInsecure bool) (*IpaIAMService, error) { +func NewIpaIAMService(rootAcc Account, host, vaultName, username, password, adminGroupCN string, enableUserPlus, isInsecure bool) (*IpaIAMService, error) { ipa := IpaIAMService{ - id: 0, - version: IpaVersion, - host: host, - vaultName: vaultName, - username: username, - password: password, - rootAcc: rootAcc, + id: 0, + version: IpaVersion, + host: host, + vaultName: vaultName, + username: username, + password: password, + adminGroupCN: adminGroupCN, + enableUserPlus: enableUserPlus, + rootAcc: rootAcc, } jar, err := cookiejar.New(nil) if err != nil { @@ -124,15 +128,16 @@ func (ipa *IpaIAMService) GetUserAccount(access string) (Account, error) { return ipa.rootAcc, nil } - req, err := ipa.newRequest("user_show/1", []string{access}, map[string]any{}) + req, err := ipa.newRequest("user_show/1", []string{access}, map[string]any{"all": true}) if err != nil { return Account{}, fmt.Errorf("ipa user_show: %w", err) } userResult := struct { - Gidnumber []string - Uidnumber []string - PidNumber []string + Gidnumber []string `json:"gidnumber"` + Uidnumber []string `json:"uidnumber"` + PidNumber []string `json:"pidnumber"` + MemberOfGroup []string `json:"memberof_group"` }{} err = ipa.rpc(req, &userResult) @@ -153,9 +158,17 @@ func (ipa *IpaIAMService) GetUserAccount(access string) (Account, error) { return Account{}, err } + role := RoleUser + if ipa.enableUserPlus { + role = RoleUserPlus + } + if ipa.adminGroupCN != "" && slices.Contains(userResult.MemberOfGroup, ipa.adminGroupCN) { + role = RoleAdmin + } + account := Account{ Access: access, - Role: RoleUser, + Role: role, UserID: uid, GroupID: gid, ProjectID: pId, diff --git a/cmd/versitygw/main.go b/cmd/versitygw/main.go index 8b2d8355..a9b1399f 100644 --- a/cmd/versitygw/main.go +++ b/cmd/versitygw/main.go @@ -82,7 +82,8 @@ var ( dogstatsServers string ipaHost, ipaVaultName string ipaUser, ipaPassword string - ipaInsecure bool + ipaAdminGroupCN string + ipaEnableUserPlus, ipaInsecure bool iamDebug bool webuiPorts []string webuiCertFile, webuiKeyFile string @@ -742,6 +743,18 @@ func initFlags() []cli.Flag { EnvVars: []string{"VGW_IPA_PASSWORD"}, Destination: &ipaPassword, }, + &cli.StringFlag{ + Name: "ipa-admin-group-cn", + Usage: "FreeIPA group CN whose members are granted the admin role", + EnvVars: []string{"VGW_IPA_ADMIN_GROUP_CN"}, + Destination: &ipaAdminGroupCN, + }, + &cli.BoolFlag{ + Name: "ipa-enable-userplus", + Usage: "Grant FreeIPA users the userplus role by default instead of user", + EnvVars: []string{"VGW_IPA_ENABLE_USERPLUS"}, + Destination: &ipaEnableUserPlus, + }, &cli.BoolFlag{ Name: "ipa-insecure", Usage: "Disable verify TLS certificate of FreeIPA server", @@ -852,6 +865,8 @@ func runGateway(ctx context.Context, be backend.Backend) error { IpaVaultName: ipaVaultName, IpaUser: ipaUser, IpaPassword: ipaPassword, + IpaAdminGroupCN: ipaAdminGroupCN, + IpaEnableUserPlus: ipaEnableUserPlus, IpaInsecure: ipaInsecure, AccessLog: accessLog, LogWebhookURL: logWebhookURL, diff --git a/embedgw/embedgw.go b/embedgw/embedgw.go index 524700ea..257fdfed 100644 --- a/embedgw/embedgw.go +++ b/embedgw/embedgw.go @@ -272,6 +272,12 @@ type Config struct { IpaUser string // IpaPassword is the FreeIPA password for authentication. IpaPassword string + // IpaAdminGroupCN is the CN of the FreeIPA group whose members are granted + // the admin role. If empty, all users are assigned the user role. + IpaAdminGroupCN string + // IpaEnableUserPlus grants all FreeIPA users the userplus role by default + // instead of the user role. Members of IpaAdminGroupCN still get admin. + IpaEnableUserPlus bool // IpaInsecure disables TLS certificate verification for the FreeIPA // connection. IpaInsecure bool @@ -647,6 +653,8 @@ func RunVersityGW(ctx context.Context, be backend.Backend, cfg *Config) error { IpaVaultName: cfg.IpaVaultName, IpaUser: cfg.IpaUser, IpaPassword: cfg.IpaPassword, + IpaAdminGroupCN: cfg.IpaAdminGroupCN, + IpaEnableUserPlus: cfg.IpaEnableUserPlus, IpaInsecure: cfg.IpaInsecure, }) if err != nil { diff --git a/extra/example.conf b/extra/example.conf index caf869b2..7e8549bd 100644 --- a/extra/example.conf +++ b/extra/example.conf @@ -409,6 +409,13 @@ ROOT_SECRET_ACCESS_KEY= #VGW_IPA_USER= # Password of the user used to connect to FreeIPA #VGW_IPA_PASSWORD= +# FreeIPA group CN whose members are granted the admin role. When set, any +# FreeIPA user that is a member of this group will be assigned the admin role. +# If unset, group membership is not checked for role assignment. +#VGW_IPA_ADMIN_GROUP_CN= +# Grant all FreeIPA users the userplus role by default instead of the user role. +# Members of VGW_IPA_ADMIN_GROUP_CN still receive the admin role regardless. +#VGW_IPA_ENABLE_USERPLUS=false # Disable verify TLS certificate of FreeIPA server #VGW_IPA_INSECURE=false # FreeIPA IAM debug output