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

Implement abstraction for token authentication #12

Merged
merged 19 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 18 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
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ jobs:

- name: Build the SDK
run: go build ./...

- name: Run unit tests
run: go test ./...
60 changes: 60 additions & 0 deletions github/authentication/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package authentication

import (
"fmt"

abs "github.com/microsoft/kiota-abstractions-go"
)

const AuthorizationKey = "Authorization"
const AuthType = "bearer"
const UserAgentKey = "User-Agent"

// TODO(kfcampbell): get the version and binary name from build settings rather than hard-coding
const UserAgentValue = "go-sdk@v0.0.0"

const APIVersionKey = "X-GitHub-Api-Version"

// TODO(kfcampbell): get the version from the generated code somehow
const APIVersionValue = "2022-11-28"
kfcampbell marked this conversation as resolved.
Show resolved Hide resolved

// Request provides a wrapper around Kiota's abs.RequestInformation type
type Request struct {
*abs.RequestInformation
}

// WithAuthorization sets the Authorization header to the given token,
// prepended by the AuthType
func (r *Request) WithAuthorization(token string) {
if r.Headers.ContainsKey(AuthorizationKey) {
r.Headers.Remove(AuthorizationKey)
}
r.Headers.Add(AuthorizationKey, fmt.Sprintf("%v %v", AuthType, token))
}

// WithUserAgent allows the caller to set the User-Agent string for each request
func (r *Request) WithUserAgent(userAgent string) {
if r.Headers.ContainsKey(UserAgentKey) {
r.Headers.Remove(UserAgentKey)
}
r.Headers.Add(UserAgentKey, userAgent)
}

// WithDefaultUserAgent sets the default User-Agent string for each request
func (r *Request) WithDefaultUserAgent() {
r.WithUserAgent(UserAgentValue)
}

// WithAPIVersion sets the API version header for each request
func (r *Request) WithAPIVersion(version string) {
if r.Headers.ContainsKey(APIVersionKey) {
r.Headers.Remove(APIVersionKey)
}
r.Headers.Add(APIVersionKey, version)
}

// WithDefaultAPIVersion sets the API version header to the default (the version used
// to generate the code) for each request
func (r *Request) WithDefaultAPIVersion() {
r.WithAPIVersion(APIVersionValue)
}
85 changes: 85 additions & 0 deletions github/authentication/token_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package authentication

import (
"context"

abs "github.com/microsoft/kiota-abstractions-go"
)

type TokenProvider struct {
options []TokenProviderOption
}

type TokenProviderOption func(*TokenProvider, *Request)

// WithAuthorizationToken sets the AuthorizationToken for each request to the given token.
func WithAuthorizationToken(token string) TokenProviderOption {
return func(t *TokenProvider, r *Request) {
r.WithAuthorization(token)
}
}

// WithDefaultUserAgent sets the User-Agent string sent for requests to the default
// for this SDK.
func WithDefaultUserAgent() TokenProviderOption {
return func(t *TokenProvider, r *Request) {
r.WithDefaultUserAgent()
}
}

// WithUserAgent sets the User-Agent string sent with each request.
func WithUserAgent(userAgent string) TokenProviderOption {
return func(t *TokenProvider, r *Request) {
r.WithUserAgent(userAgent)
}
}

// WithDefaultAPIVersion sets the API version header sent with each request.
func WithDefaultAPIVersion() TokenProviderOption {
return func(t *TokenProvider, r *Request) {
r.WithDefaultAPIVersion()
}
}

// WithAPIVersion sets the API version header sent with each request.
func WithAPIVersion(version string) TokenProviderOption {
return func(t *TokenProvider, r *Request) {
r.WithAPIVersion(version)
}
}

// TODO(kfcampbell): implement new constructor with allowedHosts

// NewTokenProvider creates an instance of TokenProvider with the specified token and options.
func NewTokenProvider(options ...TokenProviderOption) *TokenProvider {
provider := &TokenProvider{
options: options,
}

return provider
}

// defaultHandlers contains our "sensible defaults" for TokenProvider initialization
var defaultHandlers = []TokenProviderOption{WithDefaultUserAgent(), WithDefaultAPIVersion()}

// AuthenticateRequest applies the default options for each request, then the user's options
// (if present in the TokenProvider). User options are guaranteed to be run in the order they
// were input.
func (t *TokenProvider) AuthenticateRequest(context context.Context, request *abs.RequestInformation, additionalAuthenticationContext map[string]interface{}) error {
reqWrapper := &Request{RequestInformation: request}

if reqWrapper.Headers == nil {
reqWrapper.Headers = abs.NewRequestHeaders()
}

for _, option := range defaultHandlers {
option(t, reqWrapper)
}

// apply user options after defaults
for _, option := range t.options {
option(t, reqWrapper)
}

return nil
}
180 changes: 180 additions & 0 deletions github/authentication/token_provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package authentication_test

import (
"context"
"fmt"
"log"
"os"
"strings"
"testing"

abstractions "github.com/microsoft/kiota-abstractions-go"
http "github.com/microsoft/kiota-http-go"
"github.com/octokit/go-sdk/github/authentication"
"github.com/octokit/go-sdk/github/octokit"
"github.com/octokit/go-sdk/github/octokit/user"
)

func TestTokenIsSetInAuthenticatedRequest(t *testing.T) {
token := "help i'm trapped in a Go binary"
provider := authentication.NewTokenProvider(authentication.WithAuthorizationToken(token))

reqInfo := abstractions.NewRequestInformation()
addtlContext := make(map[string]interface{})

err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext)
if err != nil {
t.Errorf("there should be no error when calling AuthenticateRequest")
}

if len(reqInfo.Headers.Get(authentication.AuthorizationKey)) != 1 {
t.Errorf("there should be exactly one authorization key")
}

receivedToken := reqInfo.Headers.Get(authentication.AuthorizationKey)[0]
if !strings.Contains(receivedToken, token) {
t.Errorf("received token doesn't match up with given token")
}
}

// TODO(kfcampbell): this code could be refactored to use table-based tests
func TestDefaultRequestOptions(t *testing.T) {
token := "this is not the token you're looking for"
provider := authentication.NewTokenProvider(authentication.WithAuthorizationToken(token))
reqInfo := abstractions.NewRequestInformation()
addtlContext := make(map[string]interface{})

err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext)
if err != nil {
t.Errorf("there should be no error when calling AuthenticateRequest")
}

apiVersions := reqInfo.Headers.Get(authentication.APIVersionKey)
if len(apiVersions) != 1 {
t.Errorf("exactly one API version should be present in the request")
}

if apiVersions[0] != authentication.APIVersionValue {
t.Errorf("default API version is set incorrectly")
}

userAgents := reqInfo.Headers.Get(authentication.UserAgentKey)
if len(userAgents) != 1 {
t.Errorf("exactly one user agent string should be present in the request")
}

if userAgents[0] != authentication.UserAgentValue {
t.Errorf("default user agent string is set incorrectly")
}
}

func TestOverwritingDefaultRequestOptions(t *testing.T) {
token := "i'm totally a real token"
apiVersion := "i'm totally a real API version"
userAgent := "i'm totally a real user agent"
provider := authentication.NewTokenProvider(
authentication.WithAuthorizationToken(token),
authentication.WithAPIVersion(apiVersion),
authentication.WithUserAgent(userAgent))

reqInfo := abstractions.NewRequestInformation()
addtlContext := make(map[string]interface{})

err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext)
if err != nil {
t.Errorf("should be no error when calling authenticated request")
}

apiVersions := reqInfo.Headers.Get(authentication.APIVersionKey)
if len(apiVersions) != 1 {
t.Errorf("exactly one API version should be present in the request")
}

if apiVersions[0] != apiVersion {
t.Errorf("default API version is set incorrectly")
}

userAgents := reqInfo.Headers.Get(authentication.UserAgentKey)
if len(userAgents) != 1 {
t.Errorf("exactly one user agent string should be present in the request")
}

if userAgents[0] != userAgent {
t.Errorf("default user agent string is set incorrectly")
}

}

func TestAnonymousAuthIsAllowed(t *testing.T) {
provider := authentication.NewTokenProvider()
reqInfo := abstractions.NewRequestInformation()
addtlContext := make(map[string]interface{})

err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext)
if err != nil {
t.Errorf("should be no error when calling authenticated request")
}

authorizations := reqInfo.Headers.Get(authentication.AuthorizationKey)
if len(authorizations) != 0 {
t.Errorf("no authorization header should be present in the request")
}
}

func TestTokenSetInRequestIsNotOverwritten(t *testing.T) {
providerToken := "dit dit dit / dat dat dat / dit dit dit"
provider := authentication.NewTokenProvider(
authentication.WithAuthorizationToken(providerToken),
)

requestToken := "dit dit dit dit / dit / dit dat dit dit / dit dat dat dit"
headers := abstractions.NewRequestHeaders()
headers.Add(authentication.AuthType, requestToken)

reqInfo := abstractions.NewRequestInformation()
reqInfo.Headers = headers
addtlContext := make(map[string]interface{})

err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext)
if err != nil {
t.Errorf("AuthenticateRequest should not error")
}
reqInfoToken := reqInfo.Headers.Get(authentication.AuthorizationKey)[0]

if !strings.Contains(reqInfoToken, providerToken) {
t.Errorf("received token doesn't match up with given token")
}
}

// TODO(kfcampbell): make a more permanent decision about how to structure
// and separately run unit vs. integration tests
func TestHappyPathIntegration(t *testing.T) {
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
t.Skip("in order to run integration tests, ensure a valid GITHUB_TOKEN exists in the environment")
}

provider := authentication.NewTokenProvider(
authentication.WithAuthorizationToken(token),
)

adapter, err := http.NewNetHttpRequestAdapter(provider)
if err != nil {
log.Fatalf("Error creating request adapter: %v", err)
}
headers := abstractions.NewRequestHeaders()
_ = headers.TryAdd("Accept", "application/vnd.github.v3+json")

client := octokit.NewApiClient(adapter)
emailsRequestConfig := &user.EmailsRequestBuilderGetRequestConfiguration{
Headers: headers,
}
userEmails, err := client.User().Emails().Get(context.Background(), emailsRequestConfig)
if err != nil {
log.Fatalf("%v\n", err)
}

for _, v := range userEmails {
fmt.Printf("%v\n", *v.GetEmail())
}
}
13 changes: 7 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ module github.com/octokit/go-sdk
go 1.21.4

require (
github.com/microsoft/kiota-abstractions-go v1.4.0
github.com/microsoft/kiota-abstractions-go v1.5.2
github.com/microsoft/kiota-http-go v1.1.1
github.com/microsoft/kiota-serialization-form-go v1.0.0
github.com/microsoft/kiota-serialization-json-go v1.0.4
github.com/microsoft/kiota-serialization-multipart-go v1.0.0
Expand All @@ -13,14 +14,14 @@ require (
require (
github.com/cjlapao/common-go v0.0.39 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/std-uritemplate/std-uritemplate/go v0.0.46 // indirect
github.com/std-uritemplate/std-uritemplate/go v0.0.47 // indirect
github.com/stretchr/testify v1.8.4 // indirect
go.opentelemetry.io/otel v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/otel/trace v1.19.0 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading
Loading