1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package atproto
import (
"context"
"crypto/tls"
"errors"
"fmt"
"strings"
"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/oauthclient"
"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/pdsclient"
commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1"
)
type pdsAccountCredential struct {
handle string
password string
pdsURL string
}
var errVaultRead = errors.New("marketidentity: reading the vault")
func (a *Accounts) pdsAccount(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity,
generation int64, status v1alpha1.MarketIdentityStatus) (Result, error) {
vault := a.vaultFor(workspace)
ref := oauthSessionRef(id, &status)
if ref != nil {
status.SessionRef = ref
}
credential, err := pdsAccountCredentialOf(ctx, vault, id)
if errors.Is(err, errVaultRead) {
reason := ReasonVaultUnavailable
if errors.Is(err, ErrSecretNotFound) {
reason = ReasonWaitingForCredential
}
return pending(status, generation, reason, err.Error()), err
}
if err != nil {
return failed(status, generation, "MissingCredential", err.Error()), nil
}
tlsConfig, err := a.tlsFor(ctx, workspace, id)
if err != nil {
return pending(status, generation, "TrustUnavailable", err.Error()), err
}
if ref != nil {
if session, ok := a.storedPdsSession(ctx, vault, *ref, credential.pdsURL, tlsConfig); ok {
a.holdPdsAccount(ctx, workspace, id, ref, session.DID.String(), credential.pdsURL)
return signedIn(status, generation, session.DID.String(), firstNonEmpty(session.Handle, credential.handle), credential.pdsURL, ref,
session.AccessJWT), nil
}
}
pds, err := a.remotePDS(credential.pdsURL, nil, tlsConfig)
if err != nil {
return failed(status, generation, "PdsUnavailable", err.Error()), nil
}
session, err := pds.CreateSession(ctx, credential.handle, credential.password)
if err != nil {
message := fmt.Sprintf("com.atproto.server.createSession at %s: %v", credential.pdsURL, err)
return failed(status, generation, "SignInFailed", message), errors.New(message)
}
a.log.Info("pds_session_created", "workspace", workspace, "identity", id.Name, "did", session.DID.String(), "pds", credential.pdsURL)
if ref != nil {
if err := savePdsSession(ctx, vault, *ref, credential.pdsURL, session); err != nil {
return pending(status, generation, "SessionStoreFailed", err.Error()), err
}
a.holdPdsAccount(ctx, workspace, id, ref, session.DID.String(), credential.pdsURL)
}
return signedIn(status, generation, session.DID.String(), firstNonEmpty(session.Handle, credential.handle), credential.pdsURL, ref,
session.AccessJWT), nil
}
func pdsAccountCredentialOf(ctx context.Context, vault SecretStore, id *v1alpha1.MarketIdentity) (pdsAccountCredential, error) {
if id.Spec.KeysRef == nil {
return pdsAccountCredential{}, errors.New("mode pdsAccount names no spec.keysRef; the password stays in the vault and the controller does not fall back to a local repo")
}
if vault == nil {
return pdsAccountCredential{}, errors.New("mode pdsAccount has no vault to read spec.keysRef")
}
data, err := vault.Get(ctx, *id.Spec.KeysRef)
if err != nil {
return pdsAccountCredential{}, fmt.Errorf("%w spec.keysRef %s/%s: %w", errVaultRead, id.Spec.KeysRef.Mount, id.Spec.KeysRef.Path, err)
}
credential := pdsAccountCredential{
handle: firstNonEmpty(id.Spec.Handle, firstString(data, "handle", "identifier", "user", "username")),
password: firstString(data, "password", "appPassword", "app_password"),
pdsURL: firstNonEmpty(id.Spec.PdsURL, firstString(data, "pdsUrl", "pds_url", "pds")),
}
switch {
case credential.handle == "":
return pdsAccountCredential{}, errors.New("mode pdsAccount has no handle in spec.handle or spec.keysRef")
case credential.password == "":
return pdsAccountCredential{}, errors.New("mode pdsAccount has no password in spec.keysRef; it fails closed")
case credential.pdsURL == "":
return pdsAccountCredential{}, errors.New("mode pdsAccount has no pdsUrl in spec.pdsUrl or spec.keysRef")
}
return credential, nil
}
func (a *Accounts) storedPdsSession(ctx context.Context, vault SecretStore, ref commonv1.SecretRef, pdsURL string, tlsConfig *tls.Config) (pdsclient.Session, bool) {
if vault == nil {
return pdsclient.Session{}, false
}
data, err := vault.Get(ctx, ref)
if err != nil {
return pdsclient.Session{}, false
}
access := firstString(data, "accessJwt", "accessToken", "access_token")
if !oauthclient.AccessTokenLive(access) {
return pdsclient.Session{}, false
}
tokens := pdsclient.TokenSourceFunc(func(ctx context.Context) (string, error) { return access, nil })
pds, err := a.remotePDS(pdsURL, tokens, tlsConfig)
if err != nil {
return pdsclient.Session{}, false
}
session, err := pds.GetSession(ctx)
if err != nil {
return pdsclient.Session{}, false
}
session.AccessJWT = access
session.RefreshJWT = firstString(data, "refreshJwt", "refreshToken", "refresh_token")
return session, true
}
func savePdsSession(ctx context.Context, vault SecretStore, ref commonv1.SecretRef, pdsURL string, session pdsclient.Session) error {
if vault == nil {
return errors.New("marketidentity: no vault writes the pds account session")
}
return vault.Put(ctx, ref, map[string]any{
"did": session.DID.String(),
"handle": session.Handle,
"pds": pdsURL,
"host_url": pdsURL,
"accessJwt": session.AccessJWT,
"refreshJwt": session.RefreshJWT,
"access_token": session.AccessJWT,
"refresh_token": session.RefreshJWT,
})
}
func (a *Accounts) tlsFor(ctx context.Context, workspace string, id *v1alpha1.MarketIdentity) (*tls.Config, error) {
if id.Spec.CARef == nil || id.Spec.CARef.IsZero() {
return nil, nil
}
if a.tls == nil {
return nil, errors.New("marketidentity: spec.caRef is set but no CA source is configured")
}
return a.tls(ctx, workspace, id)
}
func (a *Accounts) remotePDS(endpoint string, tokens pdsclient.TokenSource, tlsConfig *tls.Config) (pdsclient.PDS, error) {
endpoint = strings.TrimSpace(endpoint)
if endpoint == "" {
return nil, errors.New("marketidentity: the remote PDS has no URL")
}
if !strings.Contains(endpoint, "://") {
return nil, fmt.Errorf("marketidentity: the remote PDS %q is not an absolute URL", endpoint)
}
cfg := pdsclient.RemoteConfig{BaseURL: endpoint, Tokens: tokens, Timeout: requestTimeout, HTTPClient: a.resolver.HTTPClient,
TLS: a.resolver.TLS, DPoP: a.resolver.DPoP}
if a.resolver.Timeout > 0 {
cfg.Timeout = a.resolver.Timeout
}
switch {
case tlsConfig != nil:
cfg.HTTPClient, cfg.TLS = nil, tlsConfig
case cfg.HTTPClient == nil:
cfg.HTTPClient = a.http
}
return pdsclient.NewRemote(cfg)
}