Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ Prefer `make` targets at repo root:
- **BDD tests**: run `cd tests-bdd && go test ./...` (requires Docker; feature files are `tests-bdd/features/*.feature`).
- **Integration tests** may require the compose stack; follow module README(s) under `service/`.
- **README tests**: verify code examples in documentation compile and work correctly.
- **Cross-SDK e2e (xtest)**: the `opentdf/tests` repo runs the platform against the go/java/js SDKs. Use it to validate cross-language behavior (e.g. DPoP, TDF interop) that unit tests can't cover.

### Running xtest on a branch

xtest lives in `opentdf/tests` and is triggered with `gh workflow run`. Push your branch first, then point each `*-ref` input at the branch to test (use the default branch for components you didn't change). Example — testing a platform + otdfctl branch against the standard java/web SDK branches:

```bash
gh workflow run xtest.yml \
--repo opentdf/tests \
--ref main \
-f platform-ref=my-platform-branch \
-f otdfctl-ref=my-platform-branch \
-f java-ref=main \
-f js-ref=main
```

The command prints the run URL. Poll it with `gh run view <run-id> --repo opentdf/tests`, and read a job's logs with `gh run view --job=<job-id> --repo opentdf/tests --log`. When checking a specific feature, confirm its tests actually ran and were not `SKIPPED` (grep the log for the test file, e.g. `test_dpop.py`).

## Commit & Pull Request Guidelines

Expand Down
16 changes: 10 additions & 6 deletions otdfctl/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ type version struct {
BuildTime string `json:"build_time"`
SDKVersion string `json:"sdk_version"`
SchemaVersion string `json:"schema_version"`
// SupportedFeatures adds sdk.SupportedFeatures() to `--version --json`.
// This allows integrators (and opentdf/tests/xtest) to detect optional, experimental, or removed capabilities.
SupportedFeatures []string `json:"supported_features"`
}

func init() {
Expand All @@ -40,12 +43,13 @@ func init() {

if c.Flags.GetOptionalBool("version") {
v := version{
AppName: config.AppName,
Version: config.Version,
CommitSha: config.CommitSha,
BuildTime: config.BuildTime,
SDKVersion: sdk.Version,
SchemaVersion: sdk.TDFSpecVersion,
AppName: config.AppName,
Version: config.Version,
CommitSha: config.CommitSha,
BuildTime: config.BuildTime,
SDKVersion: sdk.Version,
SchemaVersion: sdk.TDFSpecVersion,
SupportedFeatures: sdk.SupportedFeatures(),
}

version := fmt.Sprintf("%s version %s (%s) %s", config.AppName, config.Version, config.BuildTime, config.CommitSha)
Expand Down
35 changes: 29 additions & 6 deletions otdfctl/pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"net"
"net/http"
"net/url"
"os"
"strconv"
Expand Down Expand Up @@ -184,7 +185,9 @@ func ValidateProfileAuthCredentials(ctx context.Context, profile *profiles.Otdfc
case "":
return ErrProfileCredentialsNotFound
case profiles.AuthTypeClientCredentials:
_, err := GetTokenWithClientCreds(ctx, profile.GetEndpoint(), c.ClientID, c.ClientSecret, profile.GetTLSNoVerify(), c.Scopes)
// Validation exercises the DPoP-bound path so it succeeds against a
// DPoP-enforcing token endpoint; the token is discarded.
_, err := GetTokenWithClientCredsDPoP(ctx, profile.GetEndpoint(), c.ClientID, c.ClientSecret, profile.GetTLSNoVerify(), c.Scopes)
if err != nil {
return err
}
Expand All @@ -204,6 +207,8 @@ func GetTokenWithProfile(ctx context.Context, profile *profiles.OtdfctlProfileSt

switch c.AuthType {
case profiles.AuthTypeClientCredentials:
// print or reuse path: return a plain bearer token (not DPoP sender-constrained)
// so it stays usable outside otdfctl. See DSPX-3998.
return GetTokenWithClientCreds(ctx, profile.GetEndpoint(), c.ClientID, c.ClientSecret, profile.GetTLSNoVerify(), c.Scopes)
case profiles.AuthTypeAccessToken:
return buildToken(&c), nil
Expand All @@ -212,12 +217,30 @@ func GetTokenWithProfile(ctx context.Context, profile *profiles.OtdfctlProfileSt
}
}

// Uses the OAuth2 client credentials flow to obtain a token.
// GetTokenWithClientCreds uses the OAuth2 client credentials flow to obtain a
// bearer token with no sender constraint.
func GetTokenWithClientCreds(ctx context.Context, endpoint string, clientID string, clientSecret string, tlsNoVerify bool, scopes []string) (*oauth2.Token, error) {
return getTokenWithClientCreds(ctx, endpoint, clientID, clientSecret, tlsNoVerify, scopes, false)
}

// GetTokenWithClientCredsDPoP uses sender-constrained tokens, which are not currently exportable.
func GetTokenWithClientCredsDPoP(ctx context.Context, endpoint string, clientID string, clientSecret string, tlsNoVerify bool, scopes []string) (*oauth2.Token, error) {
return getTokenWithClientCreds(ctx, endpoint, clientID, clientSecret, tlsNoVerify, scopes, true)
}

func getTokenWithClientCreds(ctx context.Context, endpoint string, clientID string, clientSecret string, tlsNoVerify bool, scopes []string, dpop bool) (*oauth2.Token, error) {
httpClient := utils.NewHTTPClient(tlsNoVerify)
if dpop {
var err error
httpClient, err = sdk.NewDPoPValidationHTTPClient(httpClient)
if err != nil {
return nil, err
}
}
rp, err := newOidcRelyingParty(ctx, endpoint, tlsNoVerify, oidcClientCredentials{
clientID: clientID,
clientSecret: clientSecret,
})
}, httpClient)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -337,14 +360,14 @@ func RevokeAccessToken(ctx context.Context, endpoint, clientID, refreshToken str
rp, err := newOidcRelyingParty(ctx, endpoint, tlsNoVerify, oidcClientCredentials{
clientID: clientID,
isPublic: true,
})
}, utils.NewHTTPClient(tlsNoVerify))
if err != nil {
return err
}
return oidcrp.RevokeToken(ctx, rp, refreshToken, "refresh_token")
}

func newOidcRelyingParty(ctx context.Context, endpoint string, tlsNoVerify bool, clientCreds oidcClientCredentials) (oidcrp.RelyingParty, error) {
func newOidcRelyingParty(ctx context.Context, endpoint string, tlsNoVerify bool, clientCreds oidcClientCredentials, httpClient *http.Client) (oidcrp.RelyingParty, error) {
if clientCreds.clientID == "" {
return nil, errors.New("client ID is required")
}
Expand All @@ -367,6 +390,6 @@ func newOidcRelyingParty(ctx context.Context, endpoint string, tlsNoVerify bool,
clientCreds.clientSecret,
"",
nil,
oidcrp.WithHTTPClient(utils.NewHTTPClient(tlsNoVerify)),
oidcrp.WithHTTPClient(httpClient),
)
}
Loading
Loading