Skip to content
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

Introduce entraid straming credentials providers for go-redis #1

Draft
wants to merge 43 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9b37515
wip
ndyakov Mar 13, 2025
3f284ca
wip
ndyakov Mar 17, 2025
5a5d599
wip, idp basic abstraction
ndyakov Mar 17, 2025
ca920c5
wip
ndyakov Mar 17, 2025
4d8ea71
wip
ndyakov Mar 17, 2025
5b21e16
wip
ndyakov Mar 18, 2025
2ccdfe2
add default identity provider as well
ndyakov Mar 18, 2025
04f4bf0
improvements around error handling
ndyakov Mar 18, 2025
77fe203
use idpResponse instead of plain raw token
ndyakov Mar 25, 2025
3f5cdcd
token will only expose credentials
ndyakov Mar 25, 2025
906e4c4
use token instead of authCredentials
ndyakov Mar 25, 2025
51dab12
better duration calculation:
ndyakov Mar 25, 2025
e86aae9
easier to extend
ndyakov Mar 25, 2025
90ea172
add raw token in the idp response as well
ndyakov Mar 25, 2025
0bee01a
remove unused code
ndyakov Mar 28, 2025
d3672b6
add token tests
ndyakov Mar 28, 2025
46e5fd8
add authority configuration tests
ndyakov Mar 28, 2025
aab864a
Merge remote-tracking branch 'origin/main' into intro
ndyakov Mar 31, 2025
5b6c26c
use go-redis version from commit
ndyakov Mar 31, 2025
cead391
add token manager construct tests
ndyakov Mar 31, 2025
045c4c3
play around with cover ci
ndyakov Mar 31, 2025
b245bd4
Merge remote-tracking branch 'origin/main' into intro
ndyakov Mar 31, 2025
c48eb03
push coverage
ndyakov Mar 31, 2025
af66794
add few more tests
ndyakov Apr 1, 2025
5bd20a6
add few more tests
ndyakov Apr 1, 2025
3bed8f9
fix golangci config
ndyakov Apr 1, 2025
d07a0f7
add few more tests
ndyakov Apr 1, 2025
397cbdb
add more tests
ndyakov Apr 2, 2025
bd40b61
add more tests
ndyakov Apr 2, 2025
7fb64c1
add more tests
ndyakov Apr 2, 2025
91eb076
use struct so it is easier to mock
ndyakov Apr 3, 2025
3cce203
test parse error on start
ndyakov Apr 3, 2025
329d9e7
improve testing
ndyakov Apr 7, 2025
7c86751
improve calculation of time to renewal
ndyakov Apr 7, 2025
31c438a
remove mintokenttl const
ndyakov Apr 8, 2025
c7b7533
introduce forceRefresh for GetToken
ndyakov Apr 8, 2025
40a87e0
more tests, better duration calculation
ndyakov Apr 8, 2025
6d7a9dd
improve tests and implementation
ndyakov Apr 8, 2025
a8cca57
close should not panic
ndyakov Apr 8, 2025
d37ca2c
cover token_manager to 100%
ndyakov Apr 8, 2025
3c46773
add report comment
ndyakov Apr 9, 2025
7c750e2
Merge remote-tracking branch 'origin/main' into intro
ndyakov Apr 9, 2025
73759c7
remove redundent type conversion
ndyakov Apr 9, 2025
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
4 changes: 4 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash

goimports -l -w . # includes go fmt
golangci-lint run # includes golint, go vet
2 changes: 2 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#file: noinspection GrazieInspection,GrazieInspection
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
Expand Down Expand Up @@ -55,6 +56,7 @@ jobs:
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
# noinspection GrazieInspection,GrazieInspection
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
version: "2"
linters:
disable:
- depguard

6 changes: 3 additions & 3 deletions .testcoverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ profile: cover.out
threshold:
# (optional; default 0)
# Minimum coverage percentage required for individual files.
file: 70
file: 0

# (optional; default 0)
# Minimum coverage percentage required for each package.
package: 80
package: 0

# (optional; default 0)
# Minimum overall project coverage percentage required.
total: 95
total: 10

# Holds regexp rules which will override thresholds for matched files or packages
# using their paths.
Expand Down
59 changes: 59 additions & 0 deletions authority_configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package entraid

import "fmt"

const (
// AuthorityTypeDefault is the default authority type.
// This is used to specify the authority type when requesting a token.
AuthorityTypeDefault = "default"
// AuthorityTypeMultiTenant is the multi-tenant authority type.
// This is used to specify the multi-tenant authority type when requesting a token.
// This type of authority is used to authenticate the identity when requesting a token.
AuthorityTypeMultiTenant = "multi-tenant"
// AuthorityTypeCustom is the custom authority type.
// This is used to specify the custom authority type when requesting a token.
AuthorityTypeCustom = "custom"
)

// AuthorityConfiguration represents the authority configuration for the identity provider.
// It is used to configure the authority type and authority URL when requesting a token.
type AuthorityConfiguration struct {
// AuthorityType is the type of authority used to authenticate with the identity provider.
// This can be either "default", "multi-tenant", or "custom".
AuthorityType string

// Authority is the authority used to authenticate with the identity provider.
// This is typically the URL of the identity provider.
// For example, "https://login.microsoftonline.com/{tenantID}/v2.0"
Authority string

// TenantID is the tenant ID of the identity provider.
// This is used to identify the tenant when requesting a token.
// This is typically the ID of the Azure Active Directory tenant.
TenantID string
}

// getAuthority returns the authority URL based on the authority type.
// The authority type can be either "default", "multi-tenant", or "custom".
func (a AuthorityConfiguration) getAuthority() (string, error) {
if a.AuthorityType == "" {
a.AuthorityType = AuthorityTypeDefault
}

switch a.AuthorityType {
case AuthorityTypeDefault:
return "https://login.microsoftonline.com/common", nil
case AuthorityTypeMultiTenant:
if a.TenantID == "" {
return "", fmt.Errorf("tenant ID is required when using multi-tenant authority type")
}
return fmt.Sprintf("https://login.microsoftonline.com/%s", a.TenantID), nil
case AuthorityTypeCustom:
if a.Authority == "" {
return "", fmt.Errorf("authority is required when using custom authority type")
}
return a.Authority, nil
default:
return "", fmt.Errorf("invalid authority type: %s", a.AuthorityType)
}
}
129 changes: 129 additions & 0 deletions authority_configuration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package entraid

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestAuthorityConfiguration(t *testing.T) {
tests := []struct {
name string
authorityType string
tenantID string
authority string
expected string
expectError bool
}{
{
name: "Default Authority",
authorityType: AuthorityTypeDefault,
expected: "https://login.microsoftonline.com/common",
expectError: false,
},
{
name: "Multi-Tenant Authority",
authorityType: AuthorityTypeMultiTenant,
tenantID: "12345",
expected: "https://login.microsoftonline.com/12345",
expectError: false,
},
{
name: "Custom Authority",
authorityType: AuthorityTypeCustom,
authority: "https://custom-authority.com",
expected: "https://custom-authority.com",
expectError: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ac := AuthorityConfiguration{
AuthorityType: test.authorityType,
TenantID: test.tenantID,
Authority: test.authority,
}
result, err := ac.getAuthority()
if test.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
}

func TestAuthorityConfigurationDefault(t *testing.T) {
ac := AuthorityConfiguration{}
result, err := ac.getAuthority()
assert.NoError(t, err)
assert.Equal(t, "https://login.microsoftonline.com/common", result)
}

func TestAuthorityConfigurationMultiTenant(t *testing.T) {
ac := AuthorityConfiguration{
AuthorityType: AuthorityTypeMultiTenant,
TenantID: "12345",
}
result, err := ac.getAuthority()
assert.NoError(t, err)
assert.Equal(t, "https://login.microsoftonline.com/12345", result)
}

func TestAuthorityConfigurationCustom(t *testing.T) {
ac := AuthorityConfiguration{
AuthorityType: AuthorityTypeCustom,
Authority: "https://custom-authority.com",
}
result, err := ac.getAuthority()
assert.NoError(t, err)
assert.Equal(t, "https://custom-authority.com", result)
}

func TestAuthorityConfigurationInvalid(t *testing.T) {
ac := AuthorityConfiguration{
AuthorityType: "invalid",
}
result, err := ac.getAuthority()
assert.Error(t, err)
assert.Equal(t, "", result)
}

func TestAuthorityConfigurationMissingTenantID(t *testing.T) {
ac := AuthorityConfiguration{
AuthorityType: AuthorityTypeMultiTenant,
}
result, err := ac.getAuthority()
assert.Error(t, err)
assert.Equal(t, "", result)
}

func TestAuthorityConfigurationMissingAuthority(t *testing.T) {
ac := AuthorityConfiguration{
AuthorityType: AuthorityTypeCustom,
}
result, err := ac.getAuthority()
assert.Error(t, err)
assert.Equal(t, "", result)
}

func TestAuthorityConfigurationDefaultAuthorityType(t *testing.T) {
ac := AuthorityConfiguration{
TenantID: "12345",
}
result, err := ac.getAuthority()
assert.NoError(t, err)
assert.Equal(t, "https://login.microsoftonline.com/common", result)
}

func TestAuthorityConfigurationDefaultAuthorityTypeWithTenantID(t *testing.T) {
ac := AuthorityConfiguration{
AuthorityType: AuthorityTypeDefault,
TenantID: "12345",
}
result, err := ac.getAuthority()
assert.NoError(t, err)
assert.Equal(t, "https://login.microsoftonline.com/common", result)
}
76 changes: 76 additions & 0 deletions azure_default_identity_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package entraid

import (
"context"
"fmt"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
)

// DefaultAzureIdentityProviderOptions represents the options for the DefaultAzureIdentityProvider.
type DefaultAzureIdentityProviderOptions struct {
// AzureOptions is the options used to configure the Azure identity provider.
AzureOptions *azidentity.DefaultAzureCredentialOptions
// Scopes is the list of scopes used to request a token from the identity provider.
Scopes []string

// credFactory is a factory for creating the default Azure credential.
// This is used for testing purposes, to allow mocking the credential creation.
// If not provided, the default implementation - azidentity.NewDefaultAzureCredential will be used
credFactory credFactory
}

type credFactory interface {
NewDefaultAzureCredential(options *azidentity.DefaultAzureCredentialOptions) (defaultAzureCredential, error)
}

type defaultAzureCredential interface {
GetToken(ctx context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error)
}

type defaultCredFactory struct{}

func (d *defaultCredFactory) NewDefaultAzureCredential(options *azidentity.DefaultAzureCredentialOptions) (defaultAzureCredential, error) {
return azidentity.NewDefaultAzureCredential(options)
}

type DefaultAzureIdentityProvider struct {
options *azidentity.DefaultAzureCredentialOptions
credFactory credFactory
scopes []string
}

// NewDefaultAzureIdentityProvider creates a new DefaultAzureIdentityProvider.
func NewDefaultAzureIdentityProvider(opts DefaultAzureIdentityProviderOptions) (*DefaultAzureIdentityProvider, error) {
if opts.Scopes == nil {
opts.Scopes = []string{RedisScopeDefault}
}

return &DefaultAzureIdentityProvider{
options: opts.AzureOptions,
scopes: opts.Scopes,
credFactory: opts.credFactory,
}, nil
}

// RequestToken requests a token from the Azure Default Identity provider.
// It returns the token, the expiration time, and an error if any.
func (a *DefaultAzureIdentityProvider) RequestToken() (IdentityProviderResponse, error) {
credFactory := a.credFactory
if credFactory == nil {
credFactory = &defaultCredFactory{}
}
cred, err := credFactory.NewDefaultAzureCredential(a.options)
if err != nil {
return nil, fmt.Errorf("failed to create default azure credential: %w", err)
}

token, err := cred.GetToken(context.TODO(), policy.TokenRequestOptions{Scopes: a.scopes})
if err != nil {
return nil, fmt.Errorf("failed to get token: %w", err)
}

return NewIDPResponse(ResponseTypeAccessToken, &token)
}
Loading
Loading