-
Notifications
You must be signed in to change notification settings - Fork 37
feat(otdfctl): Auth provider and profile extensions #3736
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -163,53 +163,36 @@ func ParseClaimsJWT(accessToken string) (JWTClaims, error) { | |
| return c, nil | ||
| } | ||
|
|
||
| // GetSDKAuthOptionFromProfile dispatches to the provider registered for the profile's auth type. | ||
| func GetSDKAuthOptionFromProfile(profile *profiles.OtdfctlProfileStore) (sdk.Option, error) { | ||
| c := profile.GetAuthCredentials() | ||
|
|
||
| switch c.AuthType { | ||
| case profiles.AuthTypeClientCredentials: | ||
| return sdk.WithClientCredentials(c.ClientID, c.ClientSecret, NormalizeScopes(c.Scopes)), nil | ||
| case profiles.AuthTypeAccessToken: | ||
| tokenSource := oauth2.StaticTokenSource(buildToken(&c)) | ||
| return sdk.WithOAuthAccessTokenSource(tokenSource), nil | ||
| default: | ||
| return nil, ErrInvalidAuthType | ||
| provider, err := lookupProvider(profile.GetAuthCredentials().AuthType) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return provider.SDKAuthOption(profile) | ||
| } | ||
|
|
||
| // ValidateProfileAuthCredentials dispatches to the provider registered for the profile's auth | ||
| // type. An unset auth type returns ErrProfileCredentialsNotFound. | ||
| func ValidateProfileAuthCredentials(ctx context.Context, profile *profiles.OtdfctlProfileStore) error { | ||
| c := profile.GetAuthCredentials() | ||
|
|
||
| switch c.AuthType { | ||
| case "": | ||
| authType := profile.GetAuthCredentials().AuthType | ||
| if authType == "" { | ||
| return ErrProfileCredentialsNotFound | ||
| case profiles.AuthTypeClientCredentials: | ||
| _, err := GetTokenWithClientCreds(ctx, profile.GetEndpoint(), c.ClientID, c.ClientSecret, profile.GetTLSNoVerify(), c.Scopes) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| return nil | ||
| case profiles.AuthTypeAccessToken: | ||
| if !buildToken(&c).Valid() { | ||
| return ErrAccessTokenExpired | ||
| } | ||
| default: | ||
| return ErrInvalidAuthType | ||
| } | ||
| return nil | ||
| provider, err := lookupProvider(authType) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| return provider.Validate(ctx, profile) | ||
| } | ||
|
Comment on lines
177
to
187
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a nil check for func ValidateProfileAuthCredentials(ctx context.Context, profile *profiles.OtdfctlProfileStore) error {
if profile == nil {
return ErrProfileCredentialsNotFound
}
authType := profile.GetAuthCredentials().AuthType
if authType == "" {
return ErrProfileCredentialsNotFound
}
provider, err := lookupProvider(authType)
if err != nil {
return err
}
return provider.Validate(ctx, profile)
} |
||
|
|
||
| // GetTokenWithProfile dispatches to the provider registered for the profile's auth type. | ||
| func GetTokenWithProfile(ctx context.Context, profile *profiles.OtdfctlProfileStore) (*oauth2.Token, error) { | ||
| c := profile.GetAuthCredentials() | ||
|
|
||
| switch c.AuthType { | ||
| case profiles.AuthTypeClientCredentials: | ||
| return GetTokenWithClientCreds(ctx, profile.GetEndpoint(), c.ClientID, c.ClientSecret, profile.GetTLSNoVerify(), c.Scopes) | ||
| case profiles.AuthTypeAccessToken: | ||
| return buildToken(&c), nil | ||
| default: | ||
| return nil, ErrInvalidAuthType | ||
| provider, err := lookupProvider(profile.GetAuthCredentials().AuthType) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return provider.GetToken(ctx, profile) | ||
| } | ||
|
Comment on lines
190
to
196
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a nil check for func GetTokenWithProfile(ctx context.Context, profile *profiles.OtdfctlProfileStore) (*oauth2.Token, error) {
if profile == nil {
return nil, ErrProfileCredentialsNotFound
}
provider, err := lookupProvider(profile.GetAuthCredentials().AuthType)
if err != nil {
return nil, err
}
return provider.GetToken(ctx, profile)
} |
||
|
|
||
| // Uses the OAuth2 client credentials flow to obtain a token. | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,85 @@ | ||||||||||||||||||||||||||||||||
| package auth | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| "github.com/opentdf/platform/otdfctl/pkg/profiles" | ||||||||||||||||||||||||||||||||
| "github.com/opentdf/platform/sdk" | ||||||||||||||||||||||||||||||||
| "golang.org/x/oauth2" | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
|
Comment on lines
+3
to
+9
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Import the
Suggested change
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Provider builds SDK authentication for a single auth type (keyed by | ||||||||||||||||||||||||||||||||
| // AuthCredentials.AuthType). Extending projects implement it and register it via | ||||||||||||||||||||||||||||||||
| // RegisterProvider to add a custom authentication process. | ||||||||||||||||||||||||||||||||
| type Provider interface { | ||||||||||||||||||||||||||||||||
| // SDKAuthOption returns the sdk.Option used to authenticate the SDK client. | ||||||||||||||||||||||||||||||||
| SDKAuthOption(profile *profiles.OtdfctlProfileStore) (sdk.Option, error) | ||||||||||||||||||||||||||||||||
| // Validate reports whether the profile's stored credentials are present and usable. | ||||||||||||||||||||||||||||||||
| Validate(ctx context.Context, profile *profiles.OtdfctlProfileStore) error | ||||||||||||||||||||||||||||||||
| // GetToken returns an OAuth2 token for the profile. | ||||||||||||||||||||||||||||||||
| GetToken(ctx context.Context, profile *profiles.OtdfctlProfileStore) (*oauth2.Token, error) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // authProviders maps an auth type to its provider. Registration is init-time and | ||||||||||||||||||||||||||||||||
| // single-threaded, so no locking is required. | ||||||||||||||||||||||||||||||||
| var authProviders = map[string]Provider{} | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // RegisterProvider registers (or replaces) the provider for an auth type. | ||||||||||||||||||||||||||||||||
| // Extending projects call this from init(). | ||||||||||||||||||||||||||||||||
| func RegisterProvider(authType string, provider Provider) { | ||||||||||||||||||||||||||||||||
| authProviders[authType] = provider | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // lookupProvider returns the provider for authType, or ErrInvalidAuthType if none. | ||||||||||||||||||||||||||||||||
| func lookupProvider(authType string) (Provider, error) { | ||||||||||||||||||||||||||||||||
| provider, ok := authProviders[authType] | ||||||||||||||||||||||||||||||||
| if !ok { | ||||||||||||||||||||||||||||||||
| return nil, ErrInvalidAuthType | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return provider, nil | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a var (
authProvidersMu sync.RWMutex
authProviders = map[string]Provider{}
)
// RegisterProvider registers (or replaces) the provider for an auth type.
// Extending projects call this from init().
func RegisterProvider(authType string, provider Provider) {
authProvidersMu.Lock()
defer authProvidersMu.Unlock()
authProviders[authType] = provider
}
// lookupProvider returns the provider for authType, or ErrInvalidAuthType if none.
func lookupProvider(authType string) (Provider, error) {
authProvidersMu.RLock()
defer authProvidersMu.RUnlock()
provider, ok := authProviders[authType]
if !ok {
return nil, ErrInvalidAuthType
}
return provider, nil
} |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| func init() { | ||||||||||||||||||||||||||||||||
| RegisterProvider(profiles.AuthTypeClientCredentials, clientCredentialsProvider{}) | ||||||||||||||||||||||||||||||||
| RegisterProvider(profiles.AuthTypeAccessToken, accessTokenProvider{}) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // clientCredentialsProvider implements the built-in OAuth2 client-credentials flow. | ||||||||||||||||||||||||||||||||
| type clientCredentialsProvider struct{} | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| func (clientCredentialsProvider) SDKAuthOption(profile *profiles.OtdfctlProfileStore) (sdk.Option, error) { | ||||||||||||||||||||||||||||||||
| c := profile.GetAuthCredentials() | ||||||||||||||||||||||||||||||||
| return sdk.WithClientCredentials(c.ClientID, c.ClientSecret, NormalizeScopes(c.Scopes)), nil | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| func (clientCredentialsProvider) Validate(ctx context.Context, profile *profiles.OtdfctlProfileStore) error { | ||||||||||||||||||||||||||||||||
| c := profile.GetAuthCredentials() | ||||||||||||||||||||||||||||||||
| _, err := GetTokenWithClientCreds(ctx, profile.GetEndpoint(), c.ClientID, c.ClientSecret, profile.GetTLSNoVerify(), c.Scopes) | ||||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| func (clientCredentialsProvider) GetToken(ctx context.Context, profile *profiles.OtdfctlProfileStore) (*oauth2.Token, error) { | ||||||||||||||||||||||||||||||||
| c := profile.GetAuthCredentials() | ||||||||||||||||||||||||||||||||
| return GetTokenWithClientCreds(ctx, profile.GetEndpoint(), c.ClientID, c.ClientSecret, profile.GetTLSNoVerify(), c.Scopes) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // accessTokenProvider implements the built-in static access-token flow. | ||||||||||||||||||||||||||||||||
| type accessTokenProvider struct{} | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| func (accessTokenProvider) SDKAuthOption(profile *profiles.OtdfctlProfileStore) (sdk.Option, error) { | ||||||||||||||||||||||||||||||||
| c := profile.GetAuthCredentials() | ||||||||||||||||||||||||||||||||
| return sdk.WithOAuthAccessTokenSource(oauth2.StaticTokenSource(buildToken(&c))), nil | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| func (accessTokenProvider) Validate(_ context.Context, profile *profiles.OtdfctlProfileStore) error { | ||||||||||||||||||||||||||||||||
| c := profile.GetAuthCredentials() | ||||||||||||||||||||||||||||||||
| if !buildToken(&c).Valid() { | ||||||||||||||||||||||||||||||||
| return ErrAccessTokenExpired | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| func (accessTokenProvider) GetToken(_ context.Context, profile *profiles.OtdfctlProfileStore) (*oauth2.Token, error) { | ||||||||||||||||||||||||||||||||
| c := profile.GetAuthCredentials() | ||||||||||||||||||||||||||||||||
| return buildToken(&c), nil | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| package auth | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/opentdf/platform/otdfctl/pkg/profiles" | ||
| "github.com/opentdf/platform/sdk" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| "golang.org/x/oauth2" | ||
| ) | ||
|
|
||
| type fakeProvider struct { | ||
| sdkCalled bool | ||
| validateCalled bool | ||
| getTokenCalled bool | ||
| token *oauth2.Token | ||
| } | ||
|
|
||
| func (f *fakeProvider) SDKAuthOption(_ *profiles.OtdfctlProfileStore) (sdk.Option, error) { | ||
| f.sdkCalled = true | ||
| return sdk.WithInsecurePlaintextConn(), nil | ||
| } | ||
|
|
||
| func (f *fakeProvider) Validate(_ context.Context, _ *profiles.OtdfctlProfileStore) error { | ||
| f.validateCalled = true | ||
| return nil | ||
| } | ||
|
|
||
| func (f *fakeProvider) GetToken(_ context.Context, _ *profiles.OtdfctlProfileStore) (*oauth2.Token, error) { | ||
| f.getTokenCalled = true | ||
| return f.token, nil | ||
| } | ||
|
|
||
| func newProfileWithAuthCreds(t *testing.T, creds profiles.AuthCredentials) *profiles.OtdfctlProfileStore { | ||
| t.Helper() | ||
| store, err := profiles.NewOtdfctlProfileStore(profiles.ProfileDriverMemory, &profiles.ProfileConfig{ | ||
| Name: "test", | ||
| Endpoint: "http://localhost:8080", | ||
| }, true) | ||
| require.NoError(t, err) | ||
| require.NoError(t, store.SetAuthCredentials(creds)) | ||
| return store | ||
| } | ||
|
|
||
| func TestRegisterProvider_Dispatch(t *testing.T) { | ||
| const authType = "custom-dispatch-test" | ||
| fake := &fakeProvider{token: &oauth2.Token{AccessToken: "tok"}} | ||
| RegisterProvider(authType, fake) | ||
|
|
||
| profile := newProfileWithAuthCreds(t, profiles.AuthCredentials{AuthType: authType}) | ||
|
|
||
| _, err := GetSDKAuthOptionFromProfile(profile) | ||
| require.NoError(t, err) | ||
| assert.True(t, fake.sdkCalled) | ||
|
|
||
| require.NoError(t, ValidateProfileAuthCredentials(context.Background(), profile)) | ||
| assert.True(t, fake.validateCalled) | ||
|
|
||
| tok, err := GetTokenWithProfile(context.Background(), profile) | ||
| require.NoError(t, err) | ||
| assert.True(t, fake.getTokenCalled) | ||
| assert.Equal(t, "tok", tok.AccessToken) | ||
| } | ||
|
|
||
| func TestUnknownAuthType(t *testing.T) { | ||
| profile := newProfileWithAuthCreds(t, profiles.AuthCredentials{AuthType: "not-registered"}) | ||
|
|
||
| _, err := GetSDKAuthOptionFromProfile(profile) | ||
| require.ErrorIs(t, err, ErrInvalidAuthType) | ||
|
|
||
| err = ValidateProfileAuthCredentials(context.Background(), profile) | ||
| require.ErrorIs(t, err, ErrInvalidAuthType) | ||
|
|
||
| _, err = GetTokenWithProfile(context.Background(), profile) | ||
| require.ErrorIs(t, err, ErrInvalidAuthType) | ||
| } | ||
|
|
||
| func TestEmptyAuthType_Validate(t *testing.T) { | ||
| profile := newProfileWithAuthCreds(t, profiles.AuthCredentials{AuthType: ""}) | ||
|
|
||
| err := ValidateProfileAuthCredentials(context.Background(), profile) | ||
| require.ErrorIs(t, err, ErrProfileCredentialsNotFound) | ||
| } | ||
|
|
||
| func TestBuiltinAccessTokenValidation(t *testing.T) { | ||
| valid := newProfileWithAuthCreds(t, profiles.AuthCredentials{ | ||
| AuthType: profiles.AuthTypeAccessToken, | ||
| AccessToken: profiles.AuthCredentialsAccessToken{ | ||
| AccessToken: "abc", | ||
| Expiration: time.Now().Add(time.Hour).Unix(), | ||
| }, | ||
| }) | ||
| require.NoError(t, ValidateProfileAuthCredentials(context.Background(), valid)) | ||
|
|
||
| expired := newProfileWithAuthCreds(t, profiles.AuthCredentials{ | ||
| AuthType: profiles.AuthTypeAccessToken, | ||
| AccessToken: profiles.AuthCredentialsAccessToken{ | ||
| AccessToken: "abc", | ||
| Expiration: time.Now().Add(-time.Hour).Unix(), | ||
| }, | ||
| }) | ||
| require.ErrorIs(t, ValidateProfileAuthCredentials(context.Background(), expired), ErrAccessTokenExpired) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a nil check for
profileto prevent potential nil pointer dereference panics if the function is called with a nil profile store.