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.
153 lines
4.4 KiB
Go
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)
|
|
}
|
|
}
|