okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
132f576
main
1package daemon 2 3import ( 4"context" 5"encoding/json" 6"errors" 7"io" 8"net/http" 9"net/url" 10 11"albedo/cli/internal/config" 12"albedo/cli/internal/daemon/protocol" 13) 14 15type SignIn struct { 16Provider ,Label ,Detail ,Protocol ,Kind string 17Flows []string 18Fields []FormField 19} 20type Account struct { 21Provider ,ID ,Label ,Detail ,Kind string 22Selected bool 23SelectedByProfiles []string 24} 25type SignIns struct { 26Logins []SignIn 27Accounts []Account 28} 29type StartedSignIn struct {ID ,URL ,ETag string } 30type SignInStatus struct { 31State ,Message ,ETag ,Instructions ,URL ,Provider string 32Accounts []Account 33} 34 35func accountValue (w protocol.Account )Account { 36return Account {Provider :w .Provider ,ID :w .ID ,Label :w .Label ,Detail :w .Detail ,Kind :w .Kind ,Selected :len (w .SelectedByProfiles )> 0 ,SelectedByProfiles :w .SelectedByProfiles } 37} 38func SignInList (ctx context.Context ,conn * Connection ) (SignIns ,error ) { 39var result SignIns 40err := executeRead (ctx ,conn ,operation {Capability :"provider_auth" ,Name :"list provider accounts" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) {return protocol .NewGetAuthRequest (base ) },Policy :readRecovery },func (data []byte )error { 41var w protocol.Auth 42if err := decodeRequired (data ,& w );err != nil { 43return err 44 } 45if w .Providers == nil || w .Accounts == nil { 46return fieldError ("auth" ) 47 } 48result = SignIns {Logins : []SignIn {},Accounts : []Account {}} 49for _ ,p := range w .Providers { 50result .Logins = append (result .Logins ,SignIn {Provider :p .ID ,Label :p .Label ,Detail :p .Detail ,Kind :p .Kind ,Flows :p .Flows ,Fields :p .Fields }) 51 } 52for _ ,a := range w .Accounts { 53result .Accounts = append (result .Accounts ,accountValue (a )) 54 } 55return nil 56 }) 57return result ,err 58} 59func loginValue (w protocol.Login ,etag string ) (SignInStatus ,error ) { 60if w .ID == "" || etag == "" || w .Accounts == nil { 61return SignInStatus {},fieldError ("login" ) 62 } 63switch w .State { 64case "waiting" ,"exchanging" ,"complete" ,"failed" ,"cancelled" ,"expired" : 65default : 66return SignInStatus {},fieldError ("login state" ) 67 } 68if urlText := value (w .URL );urlText != "" { 69parsed ,err := url .Parse (urlText ) 70if err != nil || parsed .Host == "" || (parsed .Scheme != "http" && parsed .Scheme != "https" ) { 71return SignInStatus {},fieldError ("login URL" ) 72 } 73 } 74r := SignInStatus {State :w .State ,Message :w .Progress ,ETag :etag ,Instructions :value (w .Instructions ),URL :value (w .URL ),Provider :w .Provider ,Accounts : []Account {}} 75if w .Failure != nil { 76r .Message = w .Failure .Detail 77 } 78for _ ,a := range w .Accounts { 79r .Accounts = append (r .Accounts ,accountValue (a )) 80 } 81return r ,nil 82} 83 84// StartSignInWithID retains the same admitted intent when a transport failure is retried. 85func StartSignInWithID (ctx context.Context ,conn * Connection ,id ,provider ,flow string ,values map [string ]json.RawMessage ) (StartedSignIn ,error ) { 86result := StartedSignIn {ID :id } 87body := protocol.LoginRequest {Provider :provider ,Flow :optionalText (flow ),Values :nonNilMap (values )} 88frozen ,err := json .Marshal (body ) 89if err != nil { 90return result ,err 91 } 92op := operation {Capability :"provider_auth" ,Name :"start provider login" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 93return protocol .NewCreateLoginRequestWithBody (base ,id ,"application/json" ,body ) 94 },Body :json .RawMessage (frozen ),Validator :& result .ETag ,Policy :noRecovery } 95for attempt := range 2 { 96err = executeMutation (ctx ,conn ,op , []int {201 ,200 },func (data []byte ,_ int )error { 97var w protocol.Login 98if err := decodeRequired (data ,& w );err != nil { 99return err 100 } 101if w .ID != id || w .Provider != provider { 102return fieldError ("login identity" ) 103 } 104status ,err := loginValue (w ,result .ETag ) 105if err != nil { 106return err 107 } 108result .URL = status .URL 109return nil 110 }) 111if err == nil { 112return result ,nil 113 } 114if _ ,uncertain := errors.AsType [* UncertainOutcomeError ](err );! uncertain { 115return result ,err 116 } 117if ctx != nil && ctx .Err ()!= nil { 118return result ,err 119 } 120status ,lookupErr := PollSignIn (ctx ,conn ,id ) 121if lookupErr == nil { 122if status .Provider != provider { 123return result ,fieldError ("login provider" ) 124 } 125result .URL ,result .ETag = status .URL ,status .ETag 126return result ,nil 127 } 128if api ,ok := errors.AsType [* APIError ](lookupErr );ok && api .StatusCode == 410 { 129return result ,lookupErr 130 } 131if attempt == 1 { 132return result ,err 133 } 134 } 135panic ("unreachable login retry budget" ) 136} 137func PollSignIn (ctx context.Context ,conn * Connection ,id string ) (SignInStatus ,error ) { 138var result SignInStatus 139var etag string 140err := executeRead (ctx ,conn ,operation {Capability :"provider_auth" ,Name :"read provider login" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) {return protocol .NewGetLoginRequest (base ,id ) },Validator :& etag ,Policy :readRecovery },func (data []byte )error { 141var w protocol.Login 142if err := decodeRequired (data ,& w );err != nil { 143return err 144 } 145if w .ID != id { 146return fieldError ("login identity" ) 147 } 148var err error 149result ,err = loginValue (w ,etag ) 150return err 151 }) 152return result ,err 153} 154func SignInInput (ctx context.Context ,conn * Connection ,id ,input ,etag string )error { 155headers ,err := observedHeaders (etag ) 156if err != nil { 157return err 158 } 159var next string 160return executeMutation (ctx ,conn ,operation {Capability :"provider_auth" ,Name :"answer provider login" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 161return protocol .NewAnswerLoginRequestWithBody (base ,id ,& protocol.AnswerLoginParams {IfMatch :etag },"application/merge-patch+json" ,body ) 162 },Body : protocol.LoginPatch {Response :& input },Headers :headers ,Validator :& next ,Policy :noRecovery }, []int {200 },func (data []byte ,_ int )error { 163var w protocol.Login 164if err := decodeRequired (data ,& w );err != nil { 165return err 166 } 167_ ,err := loginValue (w ,next ) 168return err 169 }) 170} 171func CancelSignIn (ctx context.Context ,conn * Connection ,id string )error { 172return executeMutation (ctx ,conn ,operation {Capability :"provider_auth" ,Name :"cancel provider login" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 173return protocol .NewCancelLoginRequest (base ,id ) 174 },Policy :noRecovery }, []int {204 },func ([]byte ,int )error {return nil }) 175} 176func SelectProfileAccount (ctx context.Context ,conn * Connection ,profile string ,account Account ,profiles config.Profiles )error { 177saved ,ok := profiles .Providers [profile ] 178if ! ok || saved .Extension != account .Provider { 179return errors .New ("select a profile for this account provider" ) 180 } 181saved .AccountID = & account .ID 182return SaveProvider (ctx ,conn ,profile ,saved ,profiles .ETag ) 183} 184func RemoveAccount (ctx context.Context ,conn * Connection ,id string )error { 185return executeMutation (ctx ,conn ,operation {Capability :"provider_auth" ,Name :"remove provider account" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 186return protocol .NewDeleteAccountRequest (base ,id ) 187 },Policy :noRecovery }, []int {204 },func ([]byte ,int )error {return nil }) 188} 189 190func StartProviderLogin (ctx context.Context ,conn * Connection ,provider ,flow string ,values map [string ]json.RawMessage ) (StartedSignIn ,error ) { 191id ,err := operationID () 192if err != nil { 193return StartedSignIn {},err 194 } 195return StartSignInWithID (ctx ,conn ,id ,provider ,flow ,values ) 196}