Files
seaweedfs/weed/s3api/s3api_embedded_iam_oidc.go
T
Chris Lu 12688c249e feat(iam): OIDC provider store + read-only IAM API
Add OIDCProviderRecord — the persisted, IAM-managed view of an OIDC
identity provider — and an OIDCProviderStore interface with memory and
filer implementations mirroring the existing role-store pattern.

The store is hydrated at boot from the static STS.Providers list so the
new IAM API surfaces the same set the STS service already validates
against. Two read-only actions land now:

- ListOpenIDConnectProviders -> ARN-only list, AWS-shape XML.
- GetOpenIDConnectProvider   -> URL, ClientIDList, ThumbprintList,
                                Tags, CreateDate.

Mutations (Create/Delete/Add-Remove ClientID/Update Thumbprint), multiple
client_ids per provider, and TLS thumbprint pinning come in Phase 2b.
2026-05-04 22:08:13 -07:00

119 lines
4.1 KiB
Go

package s3api
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"github.com/aws/aws-sdk-go/service/iam"
iamlib "github.com/seaweedfs/seaweedfs/weed/iam"
"github.com/seaweedfs/seaweedfs/weed/iam/integration"
)
// OIDC provider IAM actions handled by this file. Mutating actions are
// reserved for Phase 2b; the read-only set lands now so existing static-config
// providers become discoverable through the AWS IAM API.
const (
actionGetOpenIDConnectProvider = "GetOpenIDConnectProvider"
actionListOpenIDConnectProviders = "ListOpenIDConnectProviders"
)
// isOIDCProviderAction reports whether an action belongs to the OIDC provider
// family. Used by ExecuteAction to short-circuit the S3ApiConfiguration code
// path for actions that don't operate on it.
func isOIDCProviderAction(action string) bool {
switch action {
case actionGetOpenIDConnectProvider,
actionListOpenIDConnectProviders:
return true
default:
return false
}
}
// dispatchOIDCProviderAction handles the OIDC provider IAM actions. Returns a
// response, the IAM error if any, and a boolean indicating whether the action
// was recognised (so the caller can fall through when false).
func (e *EmbeddedIamApi) dispatchOIDCProviderAction(ctx context.Context, values url.Values) (iamlib.RequestIDSetter, *iamError, bool) {
if !isOIDCProviderAction(values.Get("Action")) {
return nil, nil, false
}
mgr := e.oidcIAMManager()
if mgr == nil {
return nil, &iamError{
Code: iam.ErrCodeServiceFailureException,
Error: errors.New("OIDC provider store not configured"),
}, true
}
switch values.Get("Action") {
case actionListOpenIDConnectProviders:
resp, err := e.listOpenIDConnectProviders(ctx, mgr)
return resp, err, true
case actionGetOpenIDConnectProvider:
resp, err := e.getOpenIDConnectProvider(ctx, mgr, values)
return resp, err, true
}
return nil, nil, false
}
func (e *EmbeddedIamApi) oidcIAMManager() *integration.IAMManager {
if e.iam == nil || e.iam.iamIntegration == nil {
return nil
}
provider, ok := e.iam.iamIntegration.(IAMManagerProvider)
if !ok {
return nil
}
return provider.GetIAMManager()
}
func (e *EmbeddedIamApi) listOpenIDConnectProviders(ctx context.Context, mgr *integration.IAMManager) (*iamlib.ListOpenIDConnectProvidersResponse, *iamError) {
records, err := mgr.ListOIDCProviders(ctx)
if err != nil {
return nil, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
resp := &iamlib.ListOpenIDConnectProvidersResponse{}
resp.ListOpenIDConnectProvidersResult.OpenIDConnectProviderList = make([]*iamlib.OpenIDConnectProviderListEntry, 0, len(records))
for _, rec := range records {
resp.ListOpenIDConnectProvidersResult.OpenIDConnectProviderList = append(
resp.ListOpenIDConnectProvidersResult.OpenIDConnectProviderList,
&iamlib.OpenIDConnectProviderListEntry{Arn: rec.ARN},
)
}
return resp, nil
}
func (e *EmbeddedIamApi) getOpenIDConnectProvider(ctx context.Context, mgr *integration.IAMManager, values url.Values) (*iamlib.GetOpenIDConnectProviderResponse, *iamError) {
arn := strings.TrimSpace(values.Get("OpenIDConnectProviderArn"))
if arn == "" {
return nil, &iamError{
Code: iam.ErrCodeInvalidInputException,
Error: fmt.Errorf("OpenIDConnectProviderArn is required"),
}
}
rec, err := mgr.GetOIDCProvider(ctx, arn)
if err != nil {
return nil, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: err}
}
resp := &iamlib.GetOpenIDConnectProviderResponse{}
resp.GetOpenIDConnectProviderResult.Url = rec.URL
resp.GetOpenIDConnectProviderResult.ClientIDList = append([]string(nil), rec.ClientIDs...)
resp.GetOpenIDConnectProviderResult.ThumbprintList = append([]string(nil), rec.Thumbprints...)
if !rec.CreatedAt.IsZero() {
// AWS uses ISO-8601; the IAM XML format accepts time.Time-string output.
resp.GetOpenIDConnectProviderResult.CreateDate = rec.CreatedAt.UTC().Format("2006-01-02T15:04:05Z")
}
if len(rec.Tags) > 0 {
tags := make([]*iamlib.IAMTag, 0, len(rec.Tags))
for k, v := range rec.Tags {
tags = append(tags, &iamlib.IAMTag{Key: k, Value: v})
}
resp.GetOpenIDConnectProviderResult.Tags = tags
}
return resp, nil
}