• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

m-lab / token-exchange / 16038684699

03 Jul 2025 12:22AM UTC coverage: 33.687% (-16.7%) from 50.397%
16038684699

Pull #6

github

robertodauria
Update go.mod/sum
Pull Request #6: WIP: Add client integration schema and datastore manager

0 of 125 new or added lines in 1 file covered. (0.0%)

5 existing lines in 2 files now uncovered.

127 of 377 relevant lines covered (33.69%)

0.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/store/integration.go
1
package store
2

3
import (
4
        "context"
5
        "crypto/rand"
6
        "encoding/base64"
7
        "errors"
8
        "fmt"
9
        "strings"
10
        "time"
11

12
        "cloud.google.com/go/datastore"
13
        "golang.org/x/crypto/bcrypt"
14
)
15

16
const (
17
        IntegratorKind        = "Integrator"
18
        IntegrationAPIKeyKind = "IntegrationAPIKey"
19
        APIKeyPrefix          = "mlabk"
20
)
21

22
var (
23
        // ErrInvalidIntegrationKey is returned when the integration API key is not found or invalid
24
        ErrInvalidIntegrationKey = errors.New("invalid integration API key")
25
        // ErrMalformedKey is returned when the API key format is incorrect
26
        ErrMalformedKey = errors.New("malformed API key")
27
        // ErrIntegratorNotFound is returned when the integrator is not found
28
        ErrIntegratorNotFound = errors.New("integrator not found")
29
        // ErrIntegratorSuspended is returned when the integrator is suspended
30
        ErrIntegratorSuspended = errors.New("integrator suspended")
31
        // ErrAPIKeyRevoked is returned when the API key is revoked
32
        ErrAPIKeyRevoked = errors.New("API key revoked")
33
)
34

35
// Integrator represents a Datastore entity for storing integrator metadata.
36
type Integrator struct {
37
        IntID        string    `datastore:"int_id"`
38
        Name         string    `datastore:"name"`
39
        Email        string    `datastore:"email"`
40
        Organization string    `datastore:"organization"`
41
        CreatedAt    time.Time `datastore:"created_at"`
42
        Status       string    `datastore:"status"` // "active" or "suspended"
43
}
44

45
// IntegrationAPIKey represents a Datastore entity for storing integration API key metadata.
46
type IntegrationAPIKey struct {
47
        IntID       string    `datastore:"int_id"`
48
        KeyID       string    `datastore:"key_id"`
49
        KeyHash     string    `datastore:"key_hash"`
50
        CreatedAt   time.Time `datastore:"created_at"`
51
        Description string    `datastore:"description"`
52
        Status      string    `datastore:"status"` // "active" or "revoked"
53
}
54

55
// IntegrationManager maintains state for managing integrators and their API keys in Datastore.
56
type IntegrationManager struct {
57
        client    DatastoreClient
58
        project   string
59
        namespace string
60
}
61

62
// NewIntegrationManager creates a new IntegrationManager instance.
NEW
63
func NewIntegrationManager(client DatastoreClient, project, ns string) *IntegrationManager {
×
NEW
64
        return &IntegrationManager{
×
NEW
65
                client:    client,
×
NEW
66
                project:   project,
×
NEW
67
                namespace: ns,
×
NEW
68
        }
×
NEW
69
}
×
70

71
// CreateIntegrator creates a new integrator entity in Datastore.
NEW
72
func (m *IntegrationManager) CreateIntegrator(ctx context.Context, intID, name, email, organization string) error {
×
NEW
73
        key := datastore.NameKey(IntegratorKind, intID, nil)
×
NEW
74
        key.Namespace = m.namespace
×
NEW
75

×
NEW
76
        integrator := &Integrator{
×
NEW
77
                IntID:        intID,
×
NEW
78
                Name:         name,
×
NEW
79
                Email:        email,
×
NEW
80
                Organization: organization,
×
NEW
81
                CreatedAt:    time.Now().UTC(),
×
NEW
82
                Status:       "active",
×
NEW
83
        }
×
NEW
84

×
NEW
85
        _, err := m.client.Put(ctx, key, integrator)
×
NEW
86
        return err
×
NEW
87
}
×
88

89
// GetIntegrator retrieves an integrator by its int_id.
NEW
90
func (m *IntegrationManager) GetIntegrator(ctx context.Context, intID string) (*Integrator, error) {
×
NEW
91
        key := datastore.NameKey(IntegratorKind, intID, nil)
×
NEW
92
        key.Namespace = m.namespace
×
NEW
93

×
NEW
94
        var integrator Integrator
×
NEW
95
        err := m.client.Get(ctx, key, &integrator)
×
NEW
96
        if err != nil {
×
NEW
97
                if err == datastore.ErrNoSuchEntity {
×
NEW
98
                        return nil, ErrIntegratorNotFound
×
NEW
99
                }
×
NEW
100
                return nil, err
×
101
        }
102

NEW
103
        return &integrator, nil
×
104
}
105

106
// CreateAPIKey creates a new integration API key for an integrator.
NEW
107
func (m *IntegrationManager) CreateAPIKey(ctx context.Context, intID, description string) (string, error) {
×
NEW
108
        // Generate key_id and key_secret
×
NEW
109
        keyID, err := generateKeyID()
×
NEW
110
        if err != nil {
×
NEW
111
                return "", fmt.Errorf("failed to generate key ID: %w", err)
×
NEW
112
        }
×
113

NEW
114
        keySecret, err := generateKeySecret()
×
NEW
115
        if err != nil {
×
NEW
116
                return "", fmt.Errorf("failed to generate key secret: %w", err)
×
NEW
117
        }
×
118

119
        // Hash the secret
NEW
120
        keyHash, err := bcrypt.GenerateFromPassword([]byte(keySecret), bcrypt.DefaultCost)
×
NEW
121
        if err != nil {
×
NEW
122
                return "", fmt.Errorf("failed to hash key secret: %w", err)
×
NEW
123
        }
×
124

125
        // Store the API key
NEW
126
        key := datastore.NameKey(IntegrationAPIKeyKind, keyID, nil)
×
NEW
127
        key.Namespace = m.namespace
×
NEW
128

×
NEW
129
        apiKey := &IntegrationAPIKey{
×
NEW
130
                IntID:       intID,
×
NEW
131
                KeyID:       keyID,
×
NEW
132
                KeyHash:     string(keyHash),
×
NEW
133
                CreatedAt:   time.Now().UTC(),
×
NEW
134
                Description: description,
×
NEW
135
                Status:      "active",
×
NEW
136
        }
×
NEW
137

×
NEW
138
        _, err = m.client.Put(ctx, key, apiKey)
×
NEW
139
        if err != nil {
×
NEW
140
                return "", err
×
NEW
141
        }
×
142

143
        // Return the full API key in the format: prefix.key_id.key_secret
NEW
144
        fullKey := fmt.Sprintf("%s.%s.%s", APIKeyPrefix, keyID, keySecret)
×
NEW
145
        return fullKey, nil
×
146
}
147

148
// ValidateKey validates an integration API key and returns the integrator ID and key ID.
NEW
149
func (m *IntegrationManager) ValidateKey(ctx context.Context, apiKey string) (string, string, error) {
×
NEW
150
        // Parse the API key
×
NEW
151
        keyID, keySecret, err := parseAPIKey(apiKey)
×
NEW
152
        if err != nil {
×
NEW
153
                return "", "", err
×
NEW
154
        }
×
155

156
        // Look up the API key by key_id
NEW
157
        key := datastore.NameKey(IntegrationAPIKeyKind, keyID, nil)
×
NEW
158
        key.Namespace = m.namespace
×
NEW
159

×
NEW
160
        var storedKey IntegrationAPIKey
×
NEW
161
        err = m.client.Get(ctx, key, &storedKey)
×
NEW
162
        if err != nil {
×
NEW
163
                if err == datastore.ErrNoSuchEntity {
×
NEW
164
                        return "", "", ErrInvalidIntegrationKey
×
NEW
165
                }
×
NEW
166
                return "", "", err
×
167
        }
168

169
        // Check if the API key is revoked
NEW
170
        if storedKey.Status != "active" {
×
NEW
171
                return "", "", ErrAPIKeyRevoked
×
NEW
172
        }
×
173

174
        // Verify the secret
NEW
175
        err = bcrypt.CompareHashAndPassword([]byte(storedKey.KeyHash), []byte(keySecret))
×
NEW
176
        if err != nil {
×
NEW
177
                return "", "", ErrInvalidIntegrationKey
×
NEW
178
        }
×
179

180
        // Check if the integrator is active
NEW
181
        integrator, err := m.GetIntegrator(ctx, storedKey.IntID)
×
NEW
182
        if err != nil {
×
NEW
183
                return "", "", err
×
NEW
184
        }
×
185

NEW
186
        if integrator.Status != "active" {
×
NEW
187
                return "", "", ErrIntegratorSuspended
×
NEW
188
        }
×
189

NEW
190
        return storedKey.IntID, storedKey.KeyID, nil
×
191
}
192

193
// parseAPIKey parses an API key in the format "prefix.key_id.key_secret" and returns key_id and key_secret.
NEW
194
func parseAPIKey(apiKey string) (string, string, error) {
×
NEW
195
        parts := strings.Split(apiKey, ".")
×
NEW
196
        if len(parts) != 3 {
×
NEW
197
                return "", "", ErrMalformedKey
×
NEW
198
        }
×
199

NEW
200
        prefix, keyID, keySecret := parts[0], parts[1], parts[2]
×
NEW
201
        if prefix != APIKeyPrefix {
×
NEW
202
                return "", "", ErrMalformedKey
×
NEW
203
        }
×
204

NEW
205
        if keyID == "" || keySecret == "" {
×
NEW
206
                return "", "", ErrMalformedKey
×
NEW
207
        }
×
208

NEW
209
        return keyID, keySecret, nil
×
210
}
211

212
// generateKeyID generates a random key ID.
NEW
213
func generateKeyID() (string, error) {
×
NEW
214
        b := make([]byte, 8) // 64 bits
×
NEW
215
        _, err := rand.Read(b)
×
NEW
216
        if err != nil {
×
NEW
217
                return "", err
×
NEW
218
        }
×
NEW
219
        return "ki_" + base64.RawURLEncoding.EncodeToString(b), nil
×
220
}
221

222
// generateKeySecret generates a random key secret.
NEW
223
func generateKeySecret() (string, error) {
×
NEW
224
        b := make([]byte, 32) // 256 bits
×
NEW
225
        _, err := rand.Read(b)
×
NEW
226
        if err != nil {
×
NEW
227
                return "", err
×
NEW
228
        }
×
NEW
229
        return base64.RawURLEncoding.EncodeToString(b), nil
×
230
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc