Files
versitygw/iamapi/response.go
T
niksis02 4756b4d236 feat: add STS web identity federation, IAM policy Condition support, and access control enforcement
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.
2026-08-15 17:49:00 +04:00

153 lines
4.4 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 (
"encoding/xml"
"net/http"
"github.com/gofiber/fiber/v3"
"github.com/versity/versitygw/debuglogger"
"github.com/versity/versitygw/iamapi/iamerr"
"github.com/versity/versitygw/iamapi/internal/iammiddleware"
"github.com/versity/versitygw/iamapi/internal/iamutil"
"github.com/versity/versitygw/iamapi/types"
"github.com/versity/versitygw/internal/httpctx"
)
var xmlhdr = []byte(xml.Header)
const (
// HeaderAmznRequestID is the response header that carries the request ID.
// Re-exported from iammiddleware so callers only need to import iamapi.
HeaderAmznRequestID = iammiddleware.HeaderAmznRequestID
maxXMLBodyLen = 4 * 1024 * 1024
)
type Response struct {
Data types.ActionResponse
Headers map[string]*string
Status int
}
type ActionHandler func(ctx fiber.Ctx) (*Response, error)
func ProcessHandlers(controller ActionHandler, handlers ...fiber.Handler) fiber.Handler {
return func(ctx fiber.Ctx) error {
if httpctx.ContextKeySkip.IsSet(ctx) {
httpctx.ContextKeySkip.Delete(ctx)
return ctx.Next()
}
for _, handler := range handlers {
if err := handler(ctx); err != nil {
return ProcessController(ctx, func(ctx fiber.Ctx) (*Response, error) {
return &Response{}, err
})
}
}
return ProcessController(ctx, controller)
}
}
func ProcessController(ctx fiber.Ctx, controller ActionHandler) error {
response, err := controller(ctx)
if response == nil {
response = &Response{}
}
SetResponseHeaders(ctx, response.Headers)
requestID := iammiddleware.EnsureRequestID(ctx)
if err != nil {
ctx.Response().Header.SetContentType(fiber.MIMEApplicationXML)
if apiErr, ok := err.(iamerr.APIError); ok {
if isSTSAction(ctx) {
apiErr = iamerr.WithNamespace(apiErr, iamerr.STSNamespace).(iamerr.APIError)
}
return ctx.Status(apiErr.StatusCode()).Send(apiErr.XMLBody(requestID))
}
debuglogger.InternalError(err)
internalErr := iamerr.GetAPIError(iamerr.ErrInternalFailure)
if isSTSAction(ctx) {
internalErr.XMLNamespace = iamerr.STSNamespace
}
return ctx.Status(internalErr.StatusCode()).Send(internalErr.XMLBody(requestID))
}
status := response.Status
if status == 0 {
status = http.StatusOK
}
if response.Data == nil {
ctx.Status(status)
return nil
}
response.Data.SetRequestID(requestID)
responseBytes, err := xml.Marshal(response.Data)
if err != nil {
debuglogger.InternalError(err)
internalErr := iamerr.GetAPIError(iamerr.ErrInternalFailure)
ctx.Response().Header.SetContentType(fiber.MIMEApplicationXML)
return ctx.Status(internalErr.StatusCode()).Send(internalErr.XMLBody(requestID))
}
msglen := len(xmlhdr) + len(responseBytes)
if msglen > maxXMLBodyLen {
debuglogger.Logf("XML encoded body len %v exceeds max len %v", msglen, maxXMLBodyLen)
internalErr := iamerr.GetAPIError(iamerr.ErrInternalFailure)
ctx.Response().Header.SetContentType(fiber.MIMEApplicationXML)
return ctx.Status(internalErr.StatusCode()).Send(internalErr.XMLBody(requestID))
}
res := make([]byte, 0, msglen)
res = append(res, xmlhdr...)
res = append(res, responseBytes...)
ctx.Response().Header.SetContentType(fiber.MIMEApplicationXML)
ctx.Response().Header.SetContentLength(msglen)
return ctx.Status(status).Send(res)
}
// isSTSAction reports whether the current request's Action is one of the
// STS actions sharing this IAM endpoint (see router.go's stsActions),
// which render both success and error responses under STS's own XML
// namespace rather than IAM's.
func isSTSAction(ctx fiber.Ctx) bool {
action, _ := iamutil.RequestParam(ctx, "Action")
return stsActions[action]
}
func SetResponseHeaders(ctx fiber.Ctx, headers map[string]*string) {
if headers == nil {
return
}
ctx.Response().Header.DisableNormalizing()
for key, val := range headers {
if val == nil || *val == "" {
continue
}
ctx.Response().Header.Add(key, *val)
}
}