aboutsummaryrefslogtreecommitdiffstats
path: root/bridge/core/auth
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2019-12-10 00:42:23 +0100
committerGitHub <noreply@github.com>2019-12-10 00:42:23 +0100
commitf1ed857cbd3a253d77b31c0c896fdc4ade40844f (patch)
treed1efe28a1fa666039bf8180bbed0202f0437910f /bridge/core/auth
parent69af7a1e0c2647c354fd9c5b55a254ba677200e1 (diff)
parent58c0e5aac97eabc02fa890123f3845ae6fe632a8 (diff)
downloadgit-bug-f1ed857cbd3a253d77b31c0c896fdc4ade40844f.tar.gz
Merge pull request #271 from MichaelMure/bridge-credentials
bridge: huge refactor to accept multiple kind of credentials
Diffstat (limited to 'bridge/core/auth')
-rw-r--r--bridge/core/auth/credential.go232
-rw-r--r--bridge/core/auth/credential_test.go109
-rw-r--r--bridge/core/auth/options.go62
-rw-r--r--bridge/core/auth/token.go95
4 files changed, 498 insertions, 0 deletions
diff --git a/bridge/core/auth/credential.go b/bridge/core/auth/credential.go
new file mode 100644
index 00000000..a462a116
--- /dev/null
+++ b/bridge/core/auth/credential.go
@@ -0,0 +1,232 @@
+package auth
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/repository"
+)
+
+const (
+ configKeyPrefix = "git-bug.auth"
+ configKeyKind = "kind"
+ configKeyUserId = "userid"
+ configKeyTarget = "target"
+ configKeyCreateTime = "createtime"
+)
+
+type CredentialKind string
+
+const (
+ KindToken CredentialKind = "token"
+ KindLoginPassword CredentialKind = "login-password"
+)
+
+var ErrCredentialNotExist = errors.New("credential doesn't exist")
+
+func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatch {
+ return entity.NewErrMultipleMatch("credential", matching)
+}
+
+type Credential interface {
+ ID() entity.Id
+ UserId() entity.Id
+ Target() string
+ Kind() CredentialKind
+ CreateTime() time.Time
+ Validate() error
+
+ // Return all the specific properties of the credential that need to be saved into the configuration.
+ // This does not include Target, User, Kind and CreateTime.
+ ToConfig() map[string]string
+}
+
+// Load loads a credential from the repo config
+func LoadWithId(repo repository.RepoConfig, id entity.Id) (Credential, error) {
+ keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
+
+ // read token config pairs
+ rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix)
+ if err != nil {
+ // Not exactly right due to the limitation of ReadAll()
+ return nil, ErrCredentialNotExist
+ }
+
+ return loadFromConfig(rawconfigs, id)
+}
+
+// LoadWithPrefix load a credential from the repo config with a prefix
+func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, error) {
+ creds, err := List(repo)
+ if err != nil {
+ return nil, err
+ }
+
+ // preallocate but empty
+ matching := make([]Credential, 0, 5)
+
+ for _, cred := range creds {
+ if cred.ID().HasPrefix(prefix) {
+ matching = append(matching, cred)
+ }
+ }
+
+ if len(matching) > 1 {
+ ids := make([]entity.Id, len(matching))
+ for i, cred := range matching {
+ ids[i] = cred.ID()
+ }
+ return nil, NewErrMultipleMatchCredential(ids)
+ }
+
+ if len(matching) == 0 {
+ return nil, ErrCredentialNotExist
+ }
+
+ return matching[0], nil
+}
+
+func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, error) {
+ keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
+
+ // trim key prefix
+ configs := make(map[string]string)
+ for key, value := range rawConfigs {
+ newKey := strings.TrimPrefix(key, keyPrefix)
+ configs[newKey] = value
+ }
+
+ var cred Credential
+
+ switch CredentialKind(configs[configKeyKind]) {
+ case KindToken:
+ cred = NewTokenFromConfig(configs)
+ case KindLoginPassword:
+ default:
+ return nil, fmt.Errorf("unknown credential type %s", configs[configKeyKind])
+ }
+
+ return cred, nil
+}
+
+// List load all existing credentials
+func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) {
+ rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".")
+ if err != nil {
+ return nil, err
+ }
+
+ re, err := regexp.Compile(configKeyPrefix + `.([^.]+).([^.]+)`)
+ if err != nil {
+ panic(err)
+ }
+
+ mapped := make(map[string]map[string]string)
+
+ for key, val := range rawConfigs {
+ res := re.FindStringSubmatch(key)
+ if res == nil {
+ continue
+ }
+ if mapped[res[1]] == nil {
+ mapped[res[1]] = make(map[string]string)
+ }
+ mapped[res[1]][res[2]] = val
+ }
+
+ matcher := matcher(opts)
+
+ var credentials []Credential
+ for id, kvs := range mapped {
+ cred, err := loadFromConfig(kvs, entity.Id(id))
+ if err != nil {
+ return nil, err
+ }
+ if matcher.Match(cred) {
+ credentials = append(credentials, cred)
+ }
+ }
+
+ return credentials, nil
+}
+
+// IdExist return whether a credential id exist or not
+func IdExist(repo repository.RepoConfig, id entity.Id) bool {
+ _, err := LoadWithId(repo, id)
+ return err == nil
+}
+
+// PrefixExist return whether a credential id prefix exist or not
+func PrefixExist(repo repository.RepoConfig, prefix string) bool {
+ _, err := LoadWithPrefix(repo, prefix)
+ return err == nil
+}
+
+// Store stores a credential in the global git config
+func Store(repo repository.RepoConfig, cred Credential) error {
+ confs := cred.ToConfig()
+
+ prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID())
+
+ // Kind
+ err := repo.GlobalConfig().StoreString(prefix+configKeyKind, string(cred.Kind()))
+ if err != nil {
+ return err
+ }
+
+ // UserId
+ err = repo.GlobalConfig().StoreString(prefix+configKeyUserId, cred.UserId().String())
+ if err != nil {
+ return err
+ }
+
+ // Target
+ err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target())
+ if err != nil {
+ return err
+ }
+
+ // CreateTime
+ err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime())
+ if err != nil {
+ return err
+ }
+
+ // Custom
+ for key, val := range confs {
+ err := repo.GlobalConfig().StoreString(prefix+key, val)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// Remove removes a credential from the global git config
+func Remove(repo repository.RepoConfig, id entity.Id) error {
+ keyPrefix := fmt.Sprintf("%s.%s", configKeyPrefix, id)
+ return repo.GlobalConfig().RemoveAll(keyPrefix)
+}
+
+/*
+ * Sorting
+ */
+
+type ById []Credential
+
+func (b ById) Len() int {
+ return len(b)
+}
+
+func (b ById) Less(i, j int) bool {
+ return b[i].ID() < b[j].ID()
+}
+
+func (b ById) Swap(i, j int) {
+ b[i], b[j] = b[j], b[i]
+}
diff --git a/bridge/core/auth/credential_test.go b/bridge/core/auth/credential_test.go
new file mode 100644
index 00000000..f91d273d
--- /dev/null
+++ b/bridge/core/auth/credential_test.go
@@ -0,0 +1,109 @@
+package auth
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/identity"
+ "github.com/MichaelMure/git-bug/repository"
+)
+
+func TestCredential(t *testing.T) {
+ repo := repository.NewMockRepoForTest()
+
+ user1 := identity.NewIdentity("user1", "email")
+ err := user1.Commit(repo)
+ assert.NoError(t, err)
+
+ user2 := identity.NewIdentity("user2", "email")
+ err = user2.Commit(repo)
+ assert.NoError(t, err)
+
+ storeToken := func(user identity.Interface, val string, target string) *Token {
+ token := NewToken(user.Id(), val, target)
+ err = Store(repo, token)
+ require.NoError(t, err)
+ return token
+ }
+
+ token := storeToken(user1, "foobar", "github")
+
+ // Store + Load
+ err = Store(repo, token)
+ assert.NoError(t, err)
+
+ token2, err := LoadWithId(repo, token.ID())
+ assert.NoError(t, err)
+ assert.Equal(t, token.createTime.Unix(), token2.CreateTime().Unix())
+ token.createTime = token2.CreateTime()
+ assert.Equal(t, token, token2)
+
+ prefix := string(token.ID())[:10]
+
+ // LoadWithPrefix
+ token3, err := LoadWithPrefix(repo, prefix)
+ assert.NoError(t, err)
+ assert.Equal(t, token.createTime.Unix(), token3.CreateTime().Unix())
+ token.createTime = token3.CreateTime()
+ assert.Equal(t, token, token3)
+
+ token4 := storeToken(user1, "foo", "gitlab")
+ token5 := storeToken(user2, "bar", "github")
+
+ // List + options
+ creds, err := List(repo, WithTarget("github"))
+ assert.NoError(t, err)
+ sameIds(t, creds, []Credential{token, token5})
+
+ creds, err = List(repo, WithTarget("gitlab"))
+ assert.NoError(t, err)
+ sameIds(t, creds, []Credential{token4})
+
+ creds, err = List(repo, WithUser(user1))
+ assert.NoError(t, err)
+ sameIds(t, creds, []Credential{token, token4})
+
+ creds, err = List(repo, WithUserId(user1.Id()))
+ assert.NoError(t, err)
+ sameIds(t, creds, []Credential{token, token4})
+
+ creds, err = List(repo, WithKind(KindToken))
+ assert.NoError(t, err)
+ sameIds(t, creds, []Credential{token, token4, token5})
+
+ creds, err = List(repo, WithKind(KindLoginPassword))
+ assert.NoError(t, err)
+ sameIds(t, creds, []Credential{})
+
+ // Exist
+ exist := IdExist(repo, token.ID())
+ assert.True(t, exist)
+
+ exist = PrefixExist(repo, prefix)
+ assert.True(t, exist)
+
+ // Remove
+ err = Remove(repo, token.ID())
+ assert.NoError(t, err)
+
+ creds, err = List(repo)
+ assert.NoError(t, err)
+ sameIds(t, creds, []Credential{token4, token5})
+}
+
+func sameIds(t *testing.T, a []Credential, b []Credential) {
+ t.Helper()
+
+ ids := func(creds []Credential) []entity.Id {
+ result := make([]entity.Id, len(creds))
+ for i, cred := range creds {
+ result[i] = cred.ID()
+ }
+ return result
+ }
+
+ assert.ElementsMatch(t, ids(a), ids(b))
+}
diff --git a/bridge/core/auth/options.go b/bridge/core/auth/options.go
new file mode 100644
index 00000000..7bcda68e
--- /dev/null
+++ b/bridge/core/auth/options.go
@@ -0,0 +1,62 @@
+package auth
+
+import (
+ "github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/identity"
+)
+
+type options struct {
+ target string
+ userId entity.Id
+ kind CredentialKind
+}
+
+type Option func(opts *options)
+
+func matcher(opts []Option) *options {
+ result := &options{}
+ for _, opt := range opts {
+ opt(result)
+ }
+ return result
+}
+
+func (opts *options) Match(cred Credential) bool {
+ if opts.target != "" && cred.Target() != opts.target {
+ return false
+ }
+
+ if opts.userId != "" && cred.UserId() != opts.userId {
+ return false
+ }
+
+ if opts.kind != "" && cred.Kind() != opts.kind {
+ return false
+ }
+
+ return true
+}
+
+func WithTarget(target string) Option {
+ return func(opts *options) {
+ opts.target = target
+ }
+}
+
+func WithUser(user identity.Interface) Option {
+ return func(opts *options) {
+ opts.userId = user.Id()
+ }
+}
+
+func WithUserId(userId entity.Id) Option {
+ return func(opts *options) {
+ opts.userId = userId
+ }
+}
+
+func WithKind(kind CredentialKind) Option {
+ return func(opts *options) {
+ opts.kind = kind
+ }
+}
diff --git a/bridge/core/auth/token.go b/bridge/core/auth/token.go
new file mode 100644
index 00000000..12a3bfc0
--- /dev/null
+++ b/bridge/core/auth/token.go
@@ -0,0 +1,95 @@
+package auth
+
+import (
+ "crypto/sha256"
+ "fmt"
+ "time"
+
+ "github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/repository"
+)
+
+const (
+ tokenValueKey = "value"
+)
+
+var _ Credential = &Token{}
+
+// Token holds an API access token data
+type Token struct {
+ userId entity.Id
+ target string
+ createTime time.Time
+ Value string
+}
+
+// NewToken instantiate a new token
+func NewToken(userId entity.Id, value, target string) *Token {
+ return &Token{
+ userId: userId,
+ target: target,
+ createTime: time.Now(),
+ Value: value,
+ }
+}
+
+func NewTokenFromConfig(conf map[string]string) *Token {
+ token := &Token{}
+
+ token.userId = entity.Id(conf[configKeyUserId])
+ token.target = conf[configKeyTarget]
+ if createTime, ok := conf[configKeyCreateTime]; ok {
+ if t, err := repository.ParseTimestamp(createTime); err == nil {
+ token.createTime = t
+ }
+ }
+
+ token.Value = conf[tokenValueKey]
+
+ return token
+}
+
+func (t *Token) ID() entity.Id {
+ sum := sha256.Sum256([]byte(t.target + t.Value))
+ return entity.Id(fmt.Sprintf("%x", sum))
+}
+
+func (t *Token) UserId() entity.Id {
+ return t.userId
+}
+
+func (t *Token) Target() string {
+ return t.target
+}
+
+func (t *Token) Kind() CredentialKind {
+ return KindToken
+}
+
+func (t *Token) CreateTime() time.Time {
+ return t.createTime
+}
+
+// Validate ensure token important fields are valid
+func (t *Token) Validate() error {
+ if t.Value == "" {
+ return fmt.Errorf("missing value")
+ }
+ if t.target == "" {
+ return fmt.Errorf("missing target")
+ }
+ if t.createTime.IsZero() || t.createTime.Equal(time.Time{}) {
+ return fmt.Errorf("missing creation time")
+ }
+ if !core.TargetExist(t.target) {
+ return fmt.Errorf("unknown target")
+ }
+ return nil
+}
+
+func (t *Token) ToConfig() map[string]string {
+ return map[string]string{
+ tokenValueKey: t.Value,
+ }
+}