mirror of
https://github.com/versity/versitygw.git
synced 2026-09-27 02:14:15 +00:00
feat: add IAM OIDC provider CRUD
Add support for `CreateOpenIDConnectProvider`, `GetOpenIDConnectProvider`, `ListOpenIDConnectProviders`, `DeleteOpenIDConnectProvider`, `AddClientIDToOpenIDConnectProvider`, `RemoveClientIDFromOpenIDConnectProvider`, and `UpdateOpenIDConnectProviderThumbprint` on both the internal and Vault storage backends, rounding out the standalone IAM service with the same OIDC identity provider management AWS IAM exposes. CreateOpenIDConnectProvider validates the issuer URL, enforces the client ID and per-provider client ID list limits, and accepts an optional ThumbprintList. When the caller omits ThumbprintList, the provider auto-fetches the thumbprint by opening an outbound TLS connection to the issuer URL and hashing its top-level CA certificate, matching real AWS behavior. This auto-fetch is configurable: it can be turned off with the `--disable-oidc-thumbprint-autofetch` CLI flag (or the `VGW_IAM_DISABLE_OIDC_THUMBPRINT_AUTOFETCH` environment variable) for restricted or air-gapped deployments where the IAM server shouldn't make outbound connections, in which case an omitted ThumbprintList is rejected instead. AddClientIDToOpenIDConnectProvider and RemoveClientIDFromOpenIDConnectProvider manage a provider's client ID list, and UpdateOpenIDConnectProviderThumbprint replaces its thumbprint list, all with the same length and format validation applied at creation time. Provider ARNs are derived from the issuer URL, and GetOpenIDConnectProvider and DeleteOpenIDConnectProvider resolve providers by ARN, returning NoSuchEntity when a provider doesn't exist. ListOpenIDConnectProviders returns the full set of stored providers. These actions are wired into the IAM API router and given their own XML response types under iamapi/types, with a new iamapi/internal/iamutil package handling URL validation, thumbprint fetching and normalization, and ARN construction shared across the controller methods.
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
"github.com/versity/versitygw/iamapi/internal/iamutil"
|
||||
"github.com/versity/versitygw/iamapi/types"
|
||||
"github.com/versity/versitygw/internal/iamstore"
|
||||
)
|
||||
@@ -63,6 +64,11 @@ type iamConfig struct {
|
||||
Roles map[string]types.Role `json:"roles"`
|
||||
// RoleNameIndex is UserNameIndex's counterpart for roles.
|
||||
RoleNameIndex map[string]string `json:"roleNameIndex"`
|
||||
|
||||
// OIDCProviders is keyed directly by the provider's Url (scheme
|
||||
// stripped, exactly as given at creation — no index needed since
|
||||
// lookup is by exact string, not a case-insensitive human name).
|
||||
OIDCProviders map[string]types.OIDCProvider `json:"oidcProviders"`
|
||||
}
|
||||
|
||||
func defaultIAMConfig() iamConfig {
|
||||
@@ -72,6 +78,7 @@ func defaultIAMConfig() iamConfig {
|
||||
UserNameIndex: map[string]string{},
|
||||
Roles: map[string]types.Role{},
|
||||
RoleNameIndex: map[string]string{},
|
||||
OIDCProviders: map[string]types.OIDCProvider{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +111,10 @@ func normalizeIAMConfig(conf *iamConfig) {
|
||||
conf.RoleNameIndex[key] = name
|
||||
}
|
||||
}
|
||||
|
||||
if conf.OIDCProviders == nil {
|
||||
conf.OIDCProviders = make(map[string]types.OIDCProvider)
|
||||
}
|
||||
}
|
||||
|
||||
// lookupUser resolves name to the canonical stored user name and entry,
|
||||
@@ -983,3 +994,182 @@ func cloneRole(role types.Role) *types.Role {
|
||||
cloned.Policies.Inline = slices.Clone(role.Policies.Inline)
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func (s *InternalStore) CreateOIDCProvider(_ context.Context, provider types.OIDCProvider) (*types.OIDCProvider, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
if err := s.engine.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
conf, err := s.engine.ParseIAM(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, ok := conf.OIDCProviders[provider.Url]; ok {
|
||||
return nil, iamerr.EntityAlreadyExistsOIDCProvider("https://" + provider.Url)
|
||||
}
|
||||
if len(conf.OIDCProviders) >= MaxOIDCProvidersPerAccount {
|
||||
return nil, iamerr.OIDCProvidersPerAccountLimitExceeded(MaxOIDCProvidersPerAccount)
|
||||
}
|
||||
|
||||
conf.OIDCProviders[provider.Url] = provider
|
||||
return json.Marshal(conf)
|
||||
}); err != nil {
|
||||
return nil, unwrapAPIError(err)
|
||||
}
|
||||
|
||||
return cloneOIDCProvider(provider), nil
|
||||
}
|
||||
|
||||
func (s *InternalStore) GetOIDCProvider(_ context.Context, arn string) (*types.OIDCProvider, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
url, err := iamutil.ParseOIDCProviderArn(arn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf, err := s.engine.GetIAM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider, ok := conf.OIDCProviders[url]
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityOIDCProviderGet(arn)
|
||||
}
|
||||
return cloneOIDCProvider(provider), nil
|
||||
}
|
||||
|
||||
func (s *InternalStore) ListOIDCProviders(_ context.Context) (*ListOIDCProvidersOutput, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
conf, err := s.engine.GetIAM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries := make([]types.OpenIDConnectProviderListEntry, 0, len(conf.OIDCProviders))
|
||||
for _, p := range conf.OIDCProviders {
|
||||
entries = append(entries, types.OpenIDConnectProviderListEntry{Arn: p.Arn})
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool { return entries[i].Arn < entries[j].Arn })
|
||||
|
||||
return &ListOIDCProvidersOutput{Providers: entries}, nil
|
||||
}
|
||||
|
||||
func (s *InternalStore) DeleteOIDCProvider(_ context.Context, arn string) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
err := s.engine.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
conf, err := s.engine.ParseIAM(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url, err := iamutil.ParseOIDCProviderArn(arn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := conf.OIDCProviders[url]; !ok {
|
||||
return nil, iamerr.NoSuchEntityOIDCProviderDelete(arn)
|
||||
}
|
||||
delete(conf.OIDCProviders, url)
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
}
|
||||
|
||||
func (s *InternalStore) AddClientIDToOIDCProvider(_ context.Context, arn, clientID string) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
err := s.engine.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
conf, err := s.engine.ParseIAM(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url, err := iamutil.ParseOIDCProviderArn(arn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provider, ok := conf.OIDCProviders[url]
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityOIDCProviderGet(arn)
|
||||
}
|
||||
|
||||
if slices.Contains(provider.ClientIDList, clientID) {
|
||||
return json.Marshal(conf)
|
||||
}
|
||||
if len(provider.ClientIDList) >= MaxClientIDsPerOIDCProvider {
|
||||
return nil, iamerr.ClientIdsPerOpenIdConnectProviderLimitExceeded(MaxClientIDsPerOIDCProvider)
|
||||
}
|
||||
provider.ClientIDList = append(provider.ClientIDList, clientID)
|
||||
conf.OIDCProviders[url] = provider
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
}
|
||||
|
||||
func (s *InternalStore) RemoveClientIDFromOIDCProvider(_ context.Context, arn, clientID string) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
err := s.engine.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
conf, err := s.engine.ParseIAM(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url, err := iamutil.ParseOIDCProviderArn(arn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provider, ok := conf.OIDCProviders[url]
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityOIDCProviderGet(arn)
|
||||
}
|
||||
|
||||
idx := slices.Index(provider.ClientIDList, clientID)
|
||||
if idx == -1 {
|
||||
return json.Marshal(conf)
|
||||
}
|
||||
provider.ClientIDList = slices.Delete(provider.ClientIDList, idx, idx+1)
|
||||
conf.OIDCProviders[url] = provider
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
}
|
||||
|
||||
func (s *InternalStore) UpdateOIDCProviderThumbprint(_ context.Context, arn string, thumbprints []string) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
err := s.engine.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
conf, err := s.engine.ParseIAM(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url, err := iamutil.ParseOIDCProviderArn(arn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provider, ok := conf.OIDCProviders[url]
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityOIDCProviderGet(arn)
|
||||
}
|
||||
provider.ThumbprintList = thumbprints
|
||||
conf.OIDCProviders[url] = provider
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
}
|
||||
|
||||
func cloneOIDCProvider(p types.OIDCProvider) *types.OIDCProvider {
|
||||
cloned := p
|
||||
cloned.ClientIDList = slices.Clone(p.ClientIDList)
|
||||
cloned.ThumbprintList = slices.Clone(p.ThumbprintList)
|
||||
cloned.Tags = slices.Clone(p.Tags)
|
||||
return &cloned
|
||||
}
|
||||
|
||||
@@ -37,6 +37,14 @@ const MaxInlinePolicyBytesPerUser = 2048
|
||||
// all of a single IAM role's inline policy documents combined
|
||||
const MaxInlinePolicyBytesPerRole = 10240
|
||||
|
||||
// MaxClientIDsPerOIDCProvider is the maximum number of client IDs a single
|
||||
// OIDC provider may hold at once
|
||||
const MaxClientIDsPerOIDCProvider = 100
|
||||
|
||||
// MaxOIDCProvidersPerAccount is the maximum number of OIDC providers a
|
||||
// single account may hold
|
||||
const MaxOIDCProvidersPerAccount = 100
|
||||
|
||||
var (
|
||||
ErrUserIDAlreadyExists = errors.New("iamapi: user id already exists")
|
||||
ErrAccessKeyIDAlreadyExists = errors.New("iamapi: access key id already exists")
|
||||
@@ -148,6 +156,10 @@ type ListRolePoliciesOutput struct {
|
||||
Marker string
|
||||
}
|
||||
|
||||
type ListOIDCProvidersOutput struct {
|
||||
Providers []types.OpenIDConnectProviderListEntry
|
||||
}
|
||||
|
||||
// Storer is the IAM API storage backend contract.
|
||||
type Storer interface {
|
||||
CreateUser(ctx context.Context, user types.User) (*types.User, error)
|
||||
@@ -177,6 +189,15 @@ type Storer interface {
|
||||
GetRolePolicy(ctx context.Context, roleName, policyName string) (*types.PolicyEntry, error)
|
||||
DeleteRolePolicy(ctx context.Context, roleName, policyName string) error
|
||||
ListRolePolicies(ctx context.Context, input ListRolePoliciesInput) (*ListRolePoliciesOutput, error)
|
||||
|
||||
// OIDC Provider CRUD
|
||||
CreateOIDCProvider(ctx context.Context, provider types.OIDCProvider) (*types.OIDCProvider, error)
|
||||
GetOIDCProvider(ctx context.Context, arn string) (*types.OIDCProvider, error)
|
||||
ListOIDCProviders(ctx context.Context) (*ListOIDCProvidersOutput, error)
|
||||
DeleteOIDCProvider(ctx context.Context, arn string) error
|
||||
AddClientIDToOIDCProvider(ctx context.Context, arn, clientID string) error
|
||||
RemoveClientIDFromOIDCProvider(ctx context.Context, arn, clientID string) error
|
||||
UpdateOIDCProviderThumbprint(ctx context.Context, arn string, thumbprints []string) error
|
||||
}
|
||||
|
||||
func unwrapAPIError(err error) error {
|
||||
|
||||
@@ -16,6 +16,7 @@ package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -28,6 +29,7 @@ import (
|
||||
vault "github.com/hashicorp/vault-client-go"
|
||||
"github.com/hashicorp/vault-client-go/schema"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
"github.com/versity/versitygw/iamapi/internal/iamutil"
|
||||
"github.com/versity/versitygw/iamapi/types"
|
||||
)
|
||||
|
||||
@@ -1119,6 +1121,296 @@ func parseVaultRole(data map[string]any, roleName string) (types.Role, error) {
|
||||
return role, nil
|
||||
}
|
||||
|
||||
// oidcProvidersPath is the KV prefix under which OIDC providers are stored,
|
||||
// kept distinct from secretStoragePath/rolesPath.
|
||||
func (s *VaultStore) oidcProvidersPath() string {
|
||||
return s.secretStoragePath + "/oidc-providers"
|
||||
}
|
||||
|
||||
// oidcProviderPathSegment returns the literal KV path segment for a
|
||||
// provider identified by its scheme-stripped url. OIDC provider URLs may
|
||||
// themselves contain "/" (e.g. "host/" and "host/path" are distinct valid
|
||||
// providers) and Vault KV paths treat "/" as a path
|
||||
// separator, so — unlike RoleName/UserName, which never contain "/" and are
|
||||
// used as literal path segments directly — the raw url cannot safely be
|
||||
// used as a KV path segment. base64url-encoding (RawURLEncoding: lossless,
|
||||
// produces only [A-Za-z0-9_-], no "/" or "=" padding) collapses it to one
|
||||
// opaque, path-safe segment. The same segment is reused as the single outer
|
||||
// JSON key inside the KV secret body (a deliberate deviation from
|
||||
// roleToVaultMap/userToVaultMap's convention of keying on the
|
||||
// human-readable name — simpler here since only one identifier needs to be
|
||||
// tracked for read-back, not two).
|
||||
func oidcProviderPathSegment(url string) string {
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(url))
|
||||
}
|
||||
|
||||
func (s *VaultStore) CreateOIDCProvider(_ context.Context, provider types.OIDCProvider) (*types.OIDCProvider, error) {
|
||||
segment := oidcProviderPathSegment(provider.Url)
|
||||
path := s.oidcProvidersPath() + "/" + segment
|
||||
displayURL := "https://" + provider.Url
|
||||
|
||||
resp, err := s.client.Secrets.KvV2List(context.Background(), s.oidcProvidersPath(), s.kvReqOpts...)
|
||||
if err != nil && !vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
if reauthErr := s.reAuthIfNeeded(err); reauthErr != nil {
|
||||
return nil, reauthErr
|
||||
}
|
||||
resp, err = s.client.Secrets.KvV2List(context.Background(), s.oidcProvidersPath(), s.kvReqOpts...)
|
||||
if err != nil && !vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if resp != nil {
|
||||
if slices.Contains(resp.Data.Keys, segment) {
|
||||
return nil, iamerr.EntityAlreadyExistsOIDCProvider(displayURL)
|
||||
}
|
||||
if len(resp.Data.Keys) >= MaxOIDCProvidersPerAccount {
|
||||
return nil, iamerr.OIDCProvidersPerAccountLimitExceeded(MaxOIDCProvidersPerAccount)
|
||||
}
|
||||
}
|
||||
|
||||
providerMap, err := oidcProviderToVaultMap(provider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("serialize oidc provider: %w", err)
|
||||
}
|
||||
req := schema.KvV2WriteRequest{
|
||||
Data: map[string]any{segment: providerMap},
|
||||
Options: map[string]any{"cas": 0},
|
||||
}
|
||||
|
||||
_, err = s.client.Secrets.KvV2Write(context.Background(), path, req, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "check-and-set") {
|
||||
return nil, iamerr.EntityAlreadyExistsOIDCProvider(displayURL)
|
||||
}
|
||||
if reauthErr := s.reAuthIfNeeded(err); reauthErr != nil {
|
||||
return nil, reauthErr
|
||||
}
|
||||
_, err = s.client.Secrets.KvV2Write(context.Background(), path, req, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "check-and-set") {
|
||||
return nil, iamerr.EntityAlreadyExistsOIDCProvider(displayURL)
|
||||
}
|
||||
if vault.IsErrorStatus(err, http.StatusForbidden) {
|
||||
return nil, fmt.Errorf("vault 403 permission denied on path %q. check KV mount path and policy. original: %w", path, err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return cloneOIDCProvider(provider), nil
|
||||
}
|
||||
|
||||
func (s *VaultStore) GetOIDCProvider(_ context.Context, arn string) (*types.OIDCProvider, error) {
|
||||
url, err := iamutil.ParseOIDCProviderArn(arn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
segment := oidcProviderPathSegment(url)
|
||||
path := s.oidcProvidersPath() + "/" + segment
|
||||
|
||||
resp, err := s.client.Secrets.KvV2Read(context.Background(), path, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return nil, iamerr.NoSuchEntityOIDCProviderGet(arn)
|
||||
}
|
||||
if reauthErr := s.reAuthIfNeeded(err); reauthErr != nil {
|
||||
return nil, reauthErr
|
||||
}
|
||||
resp, err = s.client.Secrets.KvV2Read(context.Background(), path, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return nil, iamerr.NoSuchEntityOIDCProviderGet(arn)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
provider, err := parseVaultOIDCProvider(resp.Data.Data, segment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cloneOIDCProvider(provider), nil
|
||||
}
|
||||
|
||||
func (s *VaultStore) ListOIDCProviders(_ context.Context) (*ListOIDCProvidersOutput, error) {
|
||||
resp, err := s.client.Secrets.KvV2List(context.Background(), s.oidcProvidersPath(), s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return &ListOIDCProvidersOutput{Providers: []types.OpenIDConnectProviderListEntry{}}, nil
|
||||
}
|
||||
if reauthErr := s.reAuthIfNeeded(err); reauthErr != nil {
|
||||
return nil, reauthErr
|
||||
}
|
||||
resp, err = s.client.Secrets.KvV2List(context.Background(), s.oidcProvidersPath(), s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return &ListOIDCProvidersOutput{Providers: []types.OpenIDConnectProviderListEntry{}}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
entries := make([]types.OpenIDConnectProviderListEntry, 0, len(resp.Data.Keys))
|
||||
for _, segment := range resp.Data.Keys {
|
||||
// Read each secret by its already-known key rather than decoding
|
||||
// segment back to a url, populating the list from each secret's own
|
||||
// stored fields.
|
||||
path := s.oidcProvidersPath() + "/" + segment
|
||||
secretResp, err := s.client.Secrets.KvV2Read(context.Background(), path, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if reauthErr := s.reAuthIfNeeded(err); reauthErr != nil {
|
||||
return nil, reauthErr
|
||||
}
|
||||
secretResp, err = s.client.Secrets.KvV2Read(context.Background(), path, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
provider, err := parseVaultOIDCProvider(secretResp.Data.Data, segment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, types.OpenIDConnectProviderListEntry{Arn: provider.Arn})
|
||||
}
|
||||
|
||||
sort.Slice(entries, func(i, j int) bool { return entries[i].Arn < entries[j].Arn })
|
||||
return &ListOIDCProvidersOutput{Providers: entries}, nil
|
||||
}
|
||||
|
||||
func (s *VaultStore) DeleteOIDCProvider(_ context.Context, arn string) error {
|
||||
url, err := iamutil.ParseOIDCProviderArn(arn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := s.oidcProvidersPath() + "/" + oidcProviderPathSegment(url)
|
||||
|
||||
// Existence check first: unlike deleteRoleByPath (only reached after
|
||||
// DeleteRole's own prior GetRole existence check), Delete's own
|
||||
// not-found path is load-bearing here (NOT idempotent).
|
||||
if _, err := s.client.Secrets.KvV2Read(context.Background(), path, s.kvReqOpts...); err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return iamerr.NoSuchEntityOIDCProviderDelete(arn)
|
||||
}
|
||||
if reauthErr := s.reAuthIfNeeded(err); reauthErr != nil {
|
||||
return reauthErr
|
||||
}
|
||||
if _, err := s.client.Secrets.KvV2Read(context.Background(), path, s.kvReqOpts...); err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return iamerr.NoSuchEntityOIDCProviderDelete(arn)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return s.deleteOIDCProviderByURL(url)
|
||||
}
|
||||
|
||||
func (s *VaultStore) deleteOIDCProviderByURL(url string) error {
|
||||
path := s.oidcProvidersPath() + "/" + oidcProviderPathSegment(url)
|
||||
_, err := s.client.Secrets.KvV2DeleteMetadataAndAllVersions(context.Background(), path, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if reauthErr := s.reAuthIfNeeded(err); reauthErr != nil {
|
||||
return reauthErr
|
||||
}
|
||||
_, err = s.client.Secrets.KvV2DeleteMetadataAndAllVersions(context.Background(), path, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddClientIDToOIDCProvider / RemoveClientIDFromOIDCProvider /
|
||||
// UpdateOIDCProviderThumbprint use non-atomic get-then-replace, mirroring
|
||||
// the existing consistency model of UpdateAssumeRolePolicy/PutRolePolicy's
|
||||
// Vault implementations — this codebase has no CAS-protected
|
||||
// read-modify-write for Vault mutations today, and this does not introduce
|
||||
// one.
|
||||
|
||||
func (s *VaultStore) AddClientIDToOIDCProvider(ctx context.Context, arn, clientID string) error {
|
||||
provider, err := s.GetOIDCProvider(ctx, arn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if slices.Contains(provider.ClientIDList, clientID) {
|
||||
return nil
|
||||
}
|
||||
if len(provider.ClientIDList) >= MaxClientIDsPerOIDCProvider {
|
||||
return iamerr.ClientIdsPerOpenIdConnectProviderLimitExceeded(MaxClientIDsPerOIDCProvider)
|
||||
}
|
||||
provider.ClientIDList = append(provider.ClientIDList, clientID)
|
||||
return s.replaceOIDCProvider(ctx, *provider)
|
||||
}
|
||||
|
||||
func (s *VaultStore) RemoveClientIDFromOIDCProvider(ctx context.Context, arn, clientID string) error {
|
||||
provider, err := s.GetOIDCProvider(ctx, arn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idx := slices.Index(provider.ClientIDList, clientID)
|
||||
if idx == -1 {
|
||||
return nil
|
||||
}
|
||||
provider.ClientIDList = slices.Delete(provider.ClientIDList, idx, idx+1)
|
||||
return s.replaceOIDCProvider(ctx, *provider)
|
||||
}
|
||||
|
||||
func (s *VaultStore) UpdateOIDCProviderThumbprint(ctx context.Context, arn string, thumbprints []string) error {
|
||||
provider, err := s.GetOIDCProvider(ctx, arn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provider.ThumbprintList = thumbprints
|
||||
return s.replaceOIDCProvider(ctx, *provider)
|
||||
}
|
||||
|
||||
// replaceOIDCProvider overwrites the stored document for provider.Url by
|
||||
// deleting all existing versions and recreating with CAS=0.
|
||||
func (s *VaultStore) replaceOIDCProvider(ctx context.Context, provider types.OIDCProvider) error {
|
||||
if err := s.deleteOIDCProviderByURL(provider.Url); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := s.CreateOIDCProvider(ctx, provider)
|
||||
return err
|
||||
}
|
||||
|
||||
var errInvalidVaultOIDCProvider = errors.New("invalid oidc provider entry in vault secrets engine")
|
||||
|
||||
func oidcProviderToVaultMap(provider types.OIDCProvider) (map[string]any, error) {
|
||||
b, err := json.Marshal(provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// parseVaultOIDCProvider reconstructs an OIDCProvider from the raw
|
||||
// map[string]any vault returns. The outer key is the base64url path
|
||||
// segment used at write time (oidcProviderPathSegment), not a
|
||||
// human-readable value — unlike parseVaultRole/parseVaultUser.
|
||||
func parseVaultOIDCProvider(data map[string]any, segment string) (types.OIDCProvider, error) {
|
||||
raw, ok := data[segment]
|
||||
if !ok {
|
||||
return types.OIDCProvider{}, errInvalidVaultOIDCProvider
|
||||
}
|
||||
providerMap, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return types.OIDCProvider{}, errInvalidVaultOIDCProvider
|
||||
}
|
||||
b, err := json.Marshal(providerMap)
|
||||
if err != nil {
|
||||
return types.OIDCProvider{}, fmt.Errorf("re-marshal vault oidc provider: %w", err)
|
||||
}
|
||||
var provider types.OIDCProvider
|
||||
if err := json.Unmarshal(b, &provider); err != nil {
|
||||
return types.OIDCProvider{}, fmt.Errorf("unmarshal vault oidc provider: %w", err)
|
||||
}
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
var errInvalidVaultUser = errors.New("invalid user entry in vault secrets engine")
|
||||
|
||||
// userToVaultMap round-trips User through JSON to produce a map[string]any
|
||||
|
||||
Reference in New Issue
Block a user