mirror of
https://github.com/versity/versitygw.git
synced 2026-08-17 20:56:21 +00:00
Implements the `AssumeRoleWithWebIdentity` and `GetCallerIdentity` STS actions, letting callers exchange an external OIDC token for temporary credentials scoped to an IAM role. Token handling covers JWT claim parsing, issuer/audience resolution (including `azp` override semantics), JWKS fetching and caching with `singleflight`-deduplicated refresh, and rate-limited forced refresh on unrecognized `kid` values. OIDC provider thumbprint fetching now performs a real TLS handshake verified against the system trust store and the provider hostname (previously `InsecureSkipVerify`), since the observed certificate is persisted as a long-lived trust anchor rather than used once and discarded; all discovery-document and JWKS fetches go through an SSRF-safe HTTP client with bounded redirects and response size.
Adds policy `Condition` block evaluation, supporting `String`, `Numeric`, `Date`, `Bool`, `BinaryEquals`, and `IpAddress` operators along with their `IfExists`/`Not` variants and `ForAllValues`/`ForAnyValues` set qualifiers, plus policy variable substitution (e.g. `${aws:username}`) in supported operators. Adds identity-based inline policy evaluation and a new IAM authorization middleware that authorizes each request against action, resource, and condition context together, applying the session-policy-intersects-role-policy semantics for assumed-role sessions.
Adds a new debug logger `--log-level` flag (`silent`/`debug`/`unsafe`), along with a tree-based XML masker that redacts secrets and tokens at the property level in logged request/response bodies instead of skipping the whole body. The old `--debug/VGW_DEBUG` flag is kept as a deprecated alias for `--log-level=debug`, printing a console warning that points users at `--log-level` for finer-grained control.
Fixes a Vault storage bug where CAS (check-and-set) writes always read the current document version as 0 because `kvVersion` asserted metadata as `float64` while the Vault client actually returns `json.Number`, causing every write past the first to be rejected as a concurrent modification. Also adds a constant-time `SecureCompare` for signature/token comparisons in sigv4 auth.
Adds an integration test suite (`iam_access_control.go`) covering IAM access control across user, role, and session identities.
224 lines
5.9 KiB
Go
224 lines
5.9 KiB
Go
// Copyright 2026 Versity Software
|
|
// This file is licensed under the Apache License, Version 2.0
|
|
// (the "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package iamapi
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v3"
|
|
"github.com/gofiber/fiber/v3/middleware/logger"
|
|
"github.com/gofiber/fiber/v3/middleware/recover"
|
|
"github.com/versity/versitygw/debuglogger"
|
|
"github.com/versity/versitygw/iamapi/internal/iammiddleware"
|
|
"github.com/versity/versitygw/iamapi/storage"
|
|
"github.com/versity/versitygw/internal/netutil"
|
|
)
|
|
|
|
const (
|
|
shutDownDuration = time.Second * 10
|
|
requestHeaderMaxSize = 8 * 1024
|
|
)
|
|
|
|
// RootCredentials re-exports the type from iammiddleware so callers only need
|
|
// to import iamapi.
|
|
type RootCredentials = iammiddleware.RootCredentials
|
|
|
|
type CertStorage = netutil.CertStorage
|
|
|
|
func NewCertStorage() *CertStorage {
|
|
return netutil.NewCertStorage()
|
|
}
|
|
|
|
type IAMApiServer struct {
|
|
Router *IAMApiRouter
|
|
app *fiber.App
|
|
store storage.Storer
|
|
rootCreds *RootCredentials
|
|
CertStorage *CertStorage
|
|
quiet bool
|
|
keepAlive bool
|
|
health string
|
|
maxConnections int
|
|
maxRequests int
|
|
socketPerm os.FileMode
|
|
onListen func()
|
|
// oidcThumbprintAutoFetchDisabled disables CreateOpenIDConnectProvider's
|
|
// TLS auto-fetch fallback; see WithOIDCThumbprintAutoFetchDisabled.
|
|
oidcThumbprintAutoFetchDisabled bool
|
|
}
|
|
|
|
func New(store storage.Storer, opts ...Option) (*IAMApiServer, error) {
|
|
if store == nil {
|
|
return nil, fmt.Errorf("iamapi: storer is required")
|
|
}
|
|
|
|
server := &IAMApiServer{
|
|
store: store,
|
|
Router: &IAMApiRouter{
|
|
store: store,
|
|
},
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(server)
|
|
}
|
|
|
|
app := fiber.New(fiber.Config{
|
|
AppName: "versitygw-iam",
|
|
ServerHeader: "VERSITYGW",
|
|
DisableKeepalive: !server.keepAlive,
|
|
ErrorHandler: iammiddleware.GlobalErrorHandler,
|
|
Concurrency: server.maxConnections,
|
|
ReadBufferSize: requestHeaderMaxSize,
|
|
StreamRequestBody: false,
|
|
})
|
|
|
|
server.app = app
|
|
server.Router.app = app
|
|
server.Router.rootCreds = server.rootCreds
|
|
server.Router.oidcThumbprintAutoFetchDisabled = server.oidcThumbprintAutoFetchDisabled
|
|
|
|
app.Use("*", recover.New(recover.Config{
|
|
EnableStackTrace: true,
|
|
StackTraceHandler: iammiddleware.StackTraceHandler,
|
|
}))
|
|
|
|
if !server.quiet {
|
|
app.Use("*", logger.New(logger.Config{
|
|
Format: "${time} | vgw-iam | ${status} | ${latency} | ${ip} | ${method} | ${path} | ${error} | ${queryParams}\n",
|
|
CustomTags: map[string]logger.LogFunc{
|
|
logger.TagQueryStringParams: debuglogger.RedactedQueryParamsTag,
|
|
},
|
|
}))
|
|
}
|
|
|
|
app.Use("*", iammiddleware.RequestIDs())
|
|
|
|
if server.health != "" {
|
|
app.Get(server.health, func(ctx fiber.Ctx) error {
|
|
return ctx.SendStatus(http.StatusOK)
|
|
})
|
|
}
|
|
|
|
if server.maxRequests > 0 {
|
|
app.Use("*", iammiddleware.RateLimiter(server.maxRequests))
|
|
}
|
|
|
|
if debuglogger.IsDebugEnabled() {
|
|
app.Use("*", iammiddleware.DebugLogger())
|
|
}
|
|
|
|
server.Router.Init()
|
|
|
|
return server, nil
|
|
}
|
|
|
|
type Option func(*IAMApiServer)
|
|
|
|
func WithTLS(cs *CertStorage) Option {
|
|
return func(s *IAMApiServer) { s.CertStorage = cs }
|
|
}
|
|
|
|
func WithQuiet() Option {
|
|
return func(s *IAMApiServer) { s.quiet = true }
|
|
}
|
|
|
|
func WithHealth(health string) Option {
|
|
return func(s *IAMApiServer) { s.health = health }
|
|
}
|
|
|
|
func WithKeepAlive() Option {
|
|
return func(s *IAMApiServer) { s.keepAlive = true }
|
|
}
|
|
|
|
func WithConcurrencyLimiter(maxConnections, maxRequests int) Option {
|
|
return func(s *IAMApiServer) {
|
|
s.maxConnections = maxConnections
|
|
s.maxRequests = maxRequests
|
|
}
|
|
}
|
|
|
|
func WithSocketPerm(perm os.FileMode) Option {
|
|
return func(s *IAMApiServer) { s.socketPerm = perm }
|
|
}
|
|
|
|
func WithOnListen(fn func()) Option {
|
|
return func(s *IAMApiServer) { s.onListen = fn }
|
|
}
|
|
|
|
func WithRootUserCreds(root RootCredentials) Option {
|
|
return func(s *IAMApiServer) {
|
|
s.rootCreds = &root
|
|
}
|
|
}
|
|
|
|
// WithOIDCThumbprintAutoFetchDisabled disables CreateOpenIDConnectProvider's
|
|
// TLS auto-fetch fallback for when ThumbprintList is omitted. When set, an
|
|
// omitted ThumbprintList is rejected with a MissingValue error instead of
|
|
// the gateway making an outbound TLS connection to the caller-supplied URL
|
|
// — an operational safety valve for restricted/air-gapped deployments.
|
|
func WithOIDCThumbprintAutoFetchDisabled() Option {
|
|
return func(s *IAMApiServer) { s.oidcThumbprintAutoFetchDisabled = true }
|
|
}
|
|
|
|
func (s *IAMApiServer) ServeMultiPort(ports []string) error {
|
|
if len(ports) == 0 {
|
|
return fmt.Errorf("no ports specified")
|
|
}
|
|
|
|
var listeners []net.Listener
|
|
for _, portSpec := range ports {
|
|
var ln net.Listener
|
|
var err error
|
|
|
|
if s.CertStorage != nil {
|
|
ln, err = netutil.NewMultiAddrTLSListener(fiber.NetworkTCP, portSpec, s.CertStorage.GetCertificate, netutil.ListenerOptions{SocketPerm: s.socketPerm})
|
|
} else {
|
|
ln, err = netutil.NewMultiAddrListener(fiber.NetworkTCP, portSpec, netutil.ListenerOptions{SocketPerm: s.socketPerm})
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to bind iam listener %s: %w", portSpec, err)
|
|
}
|
|
|
|
listeners = append(listeners, ln)
|
|
}
|
|
|
|
if len(listeners) == 0 {
|
|
return fmt.Errorf("failed to create any iam listeners")
|
|
}
|
|
|
|
finalListener := netutil.NewMultiListener(listeners...)
|
|
|
|
if s.onListen != nil {
|
|
fn := s.onListen
|
|
s.app.Hooks().OnListen(func(fiber.ListenData) error {
|
|
fn()
|
|
return nil
|
|
})
|
|
}
|
|
|
|
return s.app.Listener(finalListener, fiber.ListenConfig{
|
|
DisableStartupMessage: true,
|
|
})
|
|
}
|
|
|
|
func (s *IAMApiServer) Shutdown() error {
|
|
return s.app.ShutdownWithTimeout(shutDownDuration)
|
|
}
|