Skip to content

Commit

Permalink
Merge pull request #1 from portward/subject-repository
Browse files Browse the repository at this point in the history
feat: implement subject repository
  • Loading branch information
sagikazarmark authored Sep 21, 2023
2 parents 7df327a + 7b5d243 commit 4f5d481
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 14 deletions.
3 changes: 3 additions & 0 deletions .env.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
UNKEY_API_ID=
UNKEY_ROOT_KEY=
UNKEY_API_KEY=
2 changes: 2 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ if ! has nix_direnv_version || ! nix_direnv_version 2.3.0; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.3.0/direnvrc" "sha256-Dmd+j63L84wuzgyjITIfSxSD57Tx7v51DMxVZOsiUD8="
fi
use flake . --impure

dotenv_if_exists
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/.devenv/
/.direnv/
/.env
/go.work
/go.work.sum
67 changes: 66 additions & 1 deletion authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,24 @@ var _ auth.PasswordAuthenticator = Authenticator{}
//
// [Unkey]: https://unkey.dev
type Authenticator struct {
apiID string
rootKey string

url *url.URL

// TODO: make client configurable
httpClient *http.Client
}

// NewAuthenticator returns a new [Authenticator].
func NewAuthenticator(u *url.URL) Authenticator {
func NewAuthenticator(apiID string, rootKey string, u *url.URL) Authenticator {
if u == nil {
u, _ = url.Parse("https://api.unkey.dev")
}

return Authenticator{
apiID: apiID,
rootKey: rootKey,
url: u,
httpClient: http.DefaultClient,
}
Expand Down Expand Up @@ -71,6 +76,10 @@ func (a Authenticator) AuthenticatePassword(ctx context.Context, username string
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected response status: %s", resp.Status)
}

var apiResponse verifyKeyResponse
err = json.NewDecoder(resp.Body).Decode(&apiResponse)
if err != nil {
Expand Down Expand Up @@ -131,3 +140,59 @@ func (s subject) Attribute(key string) (any, bool) {
func (s subject) Attributes() map[string]any {
return maps.Clone(s.attrs)
}

func (a Authenticator) GetSubjectByID(ctx context.Context, id auth.SubjectID) (auth.Subject, error) {
query := url.Values{}
query.Add("ownerId", id.String())

u := *a.url
u.Path = fmt.Sprintf("/v1/apis/%s/keys", a.apiID)
u.RawQuery = query.Encode()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}

req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", a.rootKey))

resp, err := a.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected response status: %s", resp.Status)
}

var apiResponse listKeysResponse
err = json.NewDecoder(resp.Body).Decode(&apiResponse)
if err != nil {
return nil, err
}

if len(apiResponse.Keys) == 0 {
// TODO: subject repository should probably return something like SubjectNotFound
return nil, auth.ErrAuthenticationFailed
}

// TODO: maybe check more than just the first key (eg. merge meta, check for expirations)

return subject{
id: id,
attrs: apiResponse.Keys[0].Meta,
}, nil
}

type listKeysResponse struct {
Keys []listKeysKeyItem `json:"keys"`

// TODO: add support for rate limit
}

type listKeysKeyItem struct {
Meta map[string]any `json:"meta"`

// TODO: add support for rate limit
}
49 changes: 38 additions & 11 deletions authenticator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,50 @@ import (

// TODO: create API keys on the fly OR run unkey locally
func TestAuthenticator(t *testing.T) {
apiKey := os.Getenv("UNKEY_APIKEY")
apiID := os.Getenv("UNKEY_API_ID")
rootKey := os.Getenv("UNKEY_ROOT_KEY")
apiKey := os.Getenv("UNKEY_API_KEY")

if apiID == "" {
t.Skip("API ID is not configured")
}

if rootKey == "" {
t.Skip("root key is not configured")
}

if apiKey == "" {
t.Skip("API key is not configured")
}

authenticator := NewAuthenticator(nil)
authenticator := NewAuthenticator(apiID, rootKey, nil)

subject, err := authenticator.AuthenticatePassword(context.Background(), "token", apiKey)
require.NoError(t, err)
t.Run("PasswordAuthenticator", func(t *testing.T) {
subject, err := authenticator.AuthenticatePassword(context.Background(), "token", apiKey)
require.NoError(t, err)

expectedID := auth.SubjectIDFromString("id")
expectedMeta := map[string]any{
"group": "admin",
"roles": []any{"user", "admin"},
}
expectedID := auth.SubjectIDFromString("id")
expectedMeta := map[string]any{
"group": "admin",
"roles": []any{"user", "admin"},
}

assert.True(t, subject.ID().Equals(expectedID))
assert.Equal(t, expectedMeta, subject.Attributes())
})

t.Run("SubjectRepository", func(t *testing.T) {
subjectID := auth.SubjectIDFromString("id")

subject, err := authenticator.GetSubjectByID(context.Background(), subjectID)
require.NoError(t, err)

expectedMeta := map[string]any{
"group": "admin",
"roles": []any{"user", "admin"},
}

assert.True(t, subject.ID().Equals(expectedID))
assert.Equal(t, expectedMeta, subject.Attributes())
assert.True(t, subject.ID().Equals(subjectID))
assert.Equal(t, expectedMeta, subject.Attributes())
})
}
15 changes: 13 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package unkeyauthenticator

import (
"fmt"
"net/url"

"github.com/portward/registry-auth/auth"
Expand All @@ -10,7 +11,9 @@ import (
//
// [PasswordAuthenticatorFactory]: https://pkg.go.dev/github.com/portward/portward/config#PasswordAuthenticatorFactory
type Config struct {
URL string `mapstructure:"url"`
APIID string `mapstructure:"apiId"`
RootKey string `mapstructure:"rootKey"`
URL string `mapstructure:"url"`
}

// New returns a new [Authenticator] from the configuration.
Expand All @@ -25,10 +28,18 @@ func (c Config) New() (auth.PasswordAuthenticator, error) {
return nil, err
}

return NewAuthenticator(apiURL), nil
return NewAuthenticator(c.APIID, c.RootKey, apiURL), nil
}

// Validate validates the configuration.
func (c Config) Validate() error {
if c.APIID == "" {
return fmt.Errorf("unkey: API ID is required")
}

if c.RootKey == "" {
return fmt.Errorf("unkey: API ID is required")
}

return nil
}

0 comments on commit 4f5d481

Please sign in to comment.