package terminal import ( "context" "encoding/json" "errors" "fmt" "strings" "sync" "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/lightninglabs/lightning-node-connect/mailbox" "github.com/lightninglabs/lightning-terminal/accounts" "github.com/lightninglabs/lightning-terminal/autopilotserver" "github.com/lightninglabs/lightning-terminal/firewall" "github.com/lightninglabs/lightning-terminal/firewalldb" "github.com/lightninglabs/lightning-terminal/litrpc" "github.com/lightninglabs/lightning-terminal/perms" "github.com/lightninglabs/lightning-terminal/rules" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/macaroons" "google.golang.org/grpc" "gopkg.in/macaroon-bakery.v2/bakery" "gopkg.in/macaroon-bakery.v2/bakery/checkers" "gopkg.in/macaroon.v2" ) // readOnlyAction defines the keyword that a permission action should be set to // when the entity is set to "uri" in order to activate the special case that // will result in all read-only permissions known to lit to be added to a // session's macaroon. The purpose of the three '*'s is to make this keyword // an invalid URI and an invalid regex so that it does not ever clash with the // other special cases. const readOnlyAction = "***readonly***" // sessionRpcServer is the gRPC server for the Session RPC interface. type sessionRpcServer struct { litrpc.UnimplementedSessionsServer litrpc.UnimplementedFirewallServer litrpc.UnimplementedAutopilotServer cfg *sessionRpcServerConfig db *session.DB sessionServer *session.Server quit chan struct{} wg sync.WaitGroup stopOnce sync.Once } // sessionRpcServerConfig holds the values used to configure the // sessionRpcServer. type sessionRpcServerConfig struct { basicAuth string dbDir string grpcOptions []grpc.ServerOption registerGrpcServers func(server *grpc.Server) superMacBaker session.MacaroonBaker firstConnectionDeadline time.Duration permMgr *perms.Manager actionsDB *firewalldb.DB autopilot autopilotserver.Autopilot ruleMgrs rules.ManagerSet privMap firewalldb.NewPrivacyMapDB } // newSessionRPCServer creates a new sessionRpcServer using the passed config. func newSessionRPCServer(cfg *sessionRpcServerConfig) (*sessionRpcServer, error) { // Create an instance of the local Terminal Connect session store DB. db, err := session.NewDB(cfg.dbDir, session.DBFilename) if err != nil { return nil, fmt.Errorf("error creating session DB: %v", err) } // Create the gRPC server that handles adding/removing sessions and the // actual mailbox server that spins up the Terminal Connect server // interface. server := session.NewServer( func(opts ...grpc.ServerOption) *grpc.Server { allOpts := append(cfg.grpcOptions, opts...) grpcServer := grpc.NewServer(allOpts...) cfg.registerGrpcServers(grpcServer) return grpcServer }, ) return &sessionRpcServer{ cfg: cfg, db: db, sessionServer: server, quit: make(chan struct{}), }, nil } // start all the components necessary for the sessionRpcServer to start serving // requests. This includes resuming all non-revoked sessions. func (s *sessionRpcServer) start() error { // Start up all previously created sessions. sessions, err := s.db.ListSessions(nil) if err != nil { return fmt.Errorf("error listing sessions: %v", err) } for _, sess := range sessions { key := sess.LocalPublicKey.SerializeCompressed() if sess.Type == session.TypeAutopilot { // We only start the autopilot sessions if the autopilot // client has been enabled. if s.cfg.autopilot == nil { continue } // Do a sanity check to ensure that we have the static // remote pub key stored for this session. This should // never not be the case. if sess.RemotePublicKey == nil { log.Errorf("no static remote key found for "+ "autopilot session %x", key) continue } if sess.State != session.StateInUse && sess.State != session.StateCreated { continue } if sess.Expiry.Before(time.Now()) { continue } ctx := context.Background() ctxc, cancel := context.WithTimeout( ctx, defaultConnectTimeout, ) // Register the session with the autopilot client. perm, err := s.cfg.autopilot.ActivateSession( ctxc, sess.LocalPublicKey, ) cancel() if err != nil { log.Errorf("error activating autopilot "+ "session (%x) with the client", key, err) if perm { err := s.db.RevokeSession( sess.LocalPublicKey, ) if err != nil { log.Errorf("error revoking "+ "session: %v", err) } continue } } } if err := s.resumeSession(sess); err != nil { log.Errorf("error resuming session (%x): %v", key, err) } } return nil } // stop cleans up any sessionRpcServer resources. func (s *sessionRpcServer) stop() error { var returnErr error s.stopOnce.Do(func() { if err := s.db.Close(); err != nil { log.Errorf("Error closing session DB: %v", err) returnErr = err } s.sessionServer.Stop() close(s.quit) s.wg.Wait() }) return returnErr } // AddSession adds and starts a new Terminal Connect session. func (s *sessionRpcServer) AddSession(_ context.Context, req *litrpc.AddSessionRequest) (*litrpc.AddSessionResponse, error) { expiry := time.Unix(int64(req.ExpiryTimestampSeconds), 0) if time.Now().After(expiry) { return nil, fmt.Errorf("expiry must be in the future") } typ, err := unmarshalRPCType(req.SessionType) if err != nil { return nil, err } // Store the entity-action permission pairs in a map in order to // de-dup any repeat perms. permissions := make(map[string]map[string]struct{}) // addPerm is a closure that can be used to add entity-action pairs to // the permissions map. addPerm := func(entity, action string) { _, ok := permissions[entity] if !ok { permissions[entity] = make(map[string]struct{}) } permissions[entity][action] = struct{}{} } var caveats []macaroon.Caveat switch typ { // For the default session types we use empty caveats and permissions, // the macaroons are baked correctly when creating the session. case session.TypeMacaroonAdmin, session.TypeMacaroonReadonly: // For account based sessions we just add the account ID caveat, the // permissions are added dynamically when creating the session. case session.TypeMacaroonAccount: id, err := accounts.ParseAccountID(req.AccountId) if err != nil { return nil, fmt.Errorf("invalid account ID: %v", err) } cav := checkers.Condition(macaroons.CondLndCustom, fmt.Sprintf( "%s %x", accounts.CondAccount, id[:], )) caveats = append(caveats, macaroon.Caveat{ Id: []byte(cav), }) // For the custom macaroon type, we use the custom permissions specified // in the request. For the time being, the caveats list will be empty // for this type. case session.TypeMacaroonCustom: if len(req.MacaroonCustomPermissions) == 0 { return nil, fmt.Errorf("custom macaroon " + "permissions must be specified for the " + "custom macaroon session type") } for _, op := range req.MacaroonCustomPermissions { if op.Entity != macaroons.PermissionEntityCustomURI { addPerm(op.Entity, op.Action) continue } // If the action specified was equal to the // readOnlyAction keyword, then this is taken to mean // that the permissions for all read-only URIs should be // granted. if op.Action == readOnlyAction { readPerms := s.cfg.permMgr.ActivePermissions( true, ) for _, p := range readPerms { addPerm(p.Entity, p.Action) } continue } // First check if this is a regex URI. uris, isRegex := s.cfg.permMgr.MatchRegexURI(op.Action) if isRegex { // This is a regex URI, and so we add each of // the matching URIs returned from the // permissions' manager. for _, uri := range uris { addPerm(op.Entity, uri) } continue } // This is not a wild card URI, so just check that the // permissions' manager is aware of this URI. _, ok := s.cfg.permMgr.URIPermissions(op.Action) if !ok { return nil, fmt.Errorf("URI %s is unknown to "+ "LiT", op.Action) } addPerm(op.Entity, op.Action) } // No other types are currently supported. default: return nil, fmt.Errorf("invalid session type, only admin, " + "readonly, custom and account macaroon types supported in " + "LiT. Autopilot sessions must be added using " + "AddAutoPilotSession method") } // Collect the de-duped permissions. var uniquePermissions []bakery.Op for entity, actions := range permissions { for action := range actions { uniquePermissions = append(uniquePermissions, bakery.Op{ Entity: entity, Action: action, }) } } sess, err := session.NewSession( req.Label, typ, expiry, req.MailboxServerAddr, req.DevServer, uniquePermissions, caveats, nil, false, ) if err != nil { return nil, fmt.Errorf("error creating new session: %v", err) } if err := s.db.StoreSession(sess); err != nil { return nil, fmt.Errorf("error storing session: %v", err) } if err := s.resumeSession(sess); err != nil { return nil, fmt.Errorf("error starting session: %v", err) } rpcSession, err := s.marshalRPCSession(sess) if err != nil { return nil, fmt.Errorf("error marshaling session: %v", err) } return &litrpc.AddSessionResponse{ Session: rpcSession, }, nil } // resumeSession tries to start an existing session if it is not expired, not // revoked and a LiT session. func (s *sessionRpcServer) resumeSession(sess *session.Session) error { pubKey := sess.LocalPublicKey pubKeyBytes := pubKey.SerializeCompressed() // We only start non-revoked, non-expired LiT sessions. Everything else // we just skip. if sess.State != session.StateInUse && sess.State != session.StateCreated { log.Debugf("Not resuming session %x with state %d", pubKeyBytes, sess.State) return nil } // Don't resume an expired session. if sess.Expiry.Before(time.Now()) { log.Debugf("Not resuming session %x with expiry %s", pubKeyBytes, sess.Expiry) if err := s.db.RevokeSession(pubKey); err != nil { return fmt.Errorf("error revoking session: %v", err) } return nil } var ( caveats []macaroon.Caveat permissions []bakery.Op readOnly = sess.Type == session.TypeMacaroonReadonly ) switch sess.Type { // For the default session types we use empty caveats and permissions, // the macaroons are baked correctly when creating the session. case session.TypeMacaroonAdmin, session.TypeMacaroonReadonly: permissions = s.cfg.permMgr.ActivePermissions(readOnly) // For account based sessions we just add the account ID caveat, the // permissions are added dynamically when creating the session. case session.TypeMacaroonAccount: if sess.MacaroonRecipe == nil { return fmt.Errorf("invalid account session, expected " + "recipe to be set") } caveats = sess.MacaroonRecipe.Caveats permissions = accounts.MacaroonPermissions // For custom session types, we use the caveats and permissions that // were persisted on session creation. case session.TypeMacaroonCustom, session.TypeAutopilot: if sess.MacaroonRecipe == nil { break } permissions = sess.MacaroonRecipe.Permissions caveats = append(caveats, sess.MacaroonRecipe.Caveats...) // No other types are currently supported. default: log.Debugf("Not resuming session %x with type %d", pubKeyBytes, sess.Type) return nil } // Add the session expiry as a macaroon caveat. macExpiry := checkers.TimeBeforeCaveat(sess.Expiry) caveats = append(caveats, macaroon.Caveat{ Id: []byte(macExpiry.Condition), }) mac, err := s.cfg.superMacBaker( context.Background(), sess.MacaroonRootKey, &session.MacaroonRecipe{ Permissions: permissions, Caveats: caveats, }, ) if err != nil { log.Debugf("Not resuming session %x. Could not bake "+ "the necessary macaroon: %w", pubKeyBytes, err) return nil } var ( onNewStatus func(s mailbox.ServerStatus) firstConnTimout = make(chan struct{}) ) // If this is the first time the session is being spun up then we will // kick off a timer to revoke the session after a timeout unless an // initial connection is made. We identify such a session as one that // we do not yet have a static remote pub key for. if sess.RemotePublicKey == nil { deadline := sess.CreatedAt.Add(s.cfg.firstConnectionDeadline) if deadline.Before(time.Now()) { log.Debugf("Deadline for session %x has already "+ "passed. Revoking session", pubKeyBytes) return s.db.RevokeSession(pubKey) } // Start the deadline timer. deadlineDuration := time.Until(deadline) deadlineTimer := time.AfterFunc(deadlineDuration, func() { close(firstConnTimout) }) log.Warnf("Kicking off deadline timer for first connection "+ "for session %x. A successful connection must be "+ "made in the next %s", pubKeyBytes, deadlineDuration) var stopTimerOnce sync.Once onNewStatus = func(s mailbox.ServerStatus) { // We will only stop the timer if the server status // indicates that the client has successfully connected. if s != mailbox.ServerStatusInUse { return } // Stop the deadline timer. stopTimerOnce.Do(func() { log.Debugf("First connection for session %x "+ "made in a timely manner", sess.LocalPublicKey. SerializeCompressed()) deadlineTimer.Stop() }) } } authData := []byte(fmt.Sprintf("%s: %s", HeaderMacaroon, mac)) sessionClosedSub, err := s.sessionServer.StartSession( sess, authData, s.db.StoreSession, onNewStatus, ) if err != nil { return err } s.wg.Add(1) go func() { defer s.wg.Done() ticker := time.NewTimer(time.Until(sess.Expiry)) defer ticker.Stop() select { case <-s.quit: return case <-sessionClosedSub: return case <-ticker.C: log.Debugf("Stopping expired session %x with "+ "type %d", pubKeyBytes, sess.Type) case <-firstConnTimout: log.Debugf("Deadline exceeded for first connection "+ "for session %x. Stopping and revoking.", pubKeyBytes) } if s.cfg.autopilot != nil { ctx := context.Background() ctxc, cancel := context.WithTimeout( ctx, defaultConnectTimeout, ) s.cfg.autopilot.SessionRevoked(ctxc, pubKey) cancel() } err = s.sessionServer.StopSession(pubKey) if err != nil { log.Debugf("Error stopping session: %v", err) } err = s.db.RevokeSession(pubKey) if err != nil { log.Debugf("error revoking session: %v", err) } }() return nil } // ListSessions returns all sessions known to the session store. func (s *sessionRpcServer) ListSessions(_ context.Context, _ *litrpc.ListSessionsRequest) (*litrpc.ListSessionsResponse, error) { sessions, err := s.db.ListSessions(nil) if err != nil { return nil, fmt.Errorf("error fetching sessions: %v", err) } response := &litrpc.ListSessionsResponse{ Sessions: make([]*litrpc.Session, len(sessions)), } for idx, sess := range sessions { response.Sessions[idx], err = s.marshalRPCSession(sess) if err != nil { return nil, fmt.Errorf("error marshaling session: %v", err) } } return response, nil } // RevokeSession revokes a single session and also stops it if it is currently // active. func (s *sessionRpcServer) RevokeSession(ctx context.Context, req *litrpc.RevokeSessionRequest) (*litrpc.RevokeSessionResponse, error) { pubKey, err := btcec.ParsePubKey(req.LocalPublicKey) if err != nil { return nil, fmt.Errorf("error parsing public key: %v", err) } if err := s.db.RevokeSession(pubKey); err != nil { return nil, fmt.Errorf("error revoking session: %v", err) } if s.cfg.autopilot != nil { s.cfg.autopilot.SessionRevoked(ctx, pubKey) } // If the session expired already it might not be running anymore. So we // only log possible errors here. if err := s.sessionServer.StopSession(pubKey); err != nil { log.Debugf("Error stopping session: %v", err) } return &litrpc.RevokeSessionResponse{}, nil } // ListActions lists all actions attempted on the Litd server. func (s *sessionRpcServer) ListActions(_ context.Context, req *litrpc.ListActionsRequest) (*litrpc.ListActionsResponse, error) { // If no maximum number of actions is given, use a default of 100. if req.MaxNumActions == 0 { req.MaxNumActions = 100 } // Build a filter function based on the request values. filterFn := func(a *firewalldb.Action, reversed bool) (bool, bool) { timeStamp := uint64(a.AttemptedAt.Unix()) if req.EndTimestamp != 0 { // If actions are being considered in order and the // timestamp of this action exceeds the given end // timestamp, then there is no need to continue // traversing. if !reversed && timeStamp > req.EndTimestamp { return false, false } // If the actions are in reverse order and the timestamp // comes after the end timestamp, then the actions is // not included but the search can continue. if reversed && timeStamp > req.EndTimestamp { return false, true } } if req.StartTimestamp != 0 { // If actions are being considered in order and the // timestamp of this action comes before the given start // timestamp, then the action is not included but the // search can continue. if !reversed && timeStamp < req.StartTimestamp { return false, true } // If the actions are in reverse order and the timestamp // comes before the start timestamp, then there is no // need to continue traversing. if reversed && timeStamp < req.StartTimestamp { return false, false } } if req.FeatureName != "" && a.FeatureName != req.FeatureName { return false, true } if req.ActorName != "" && a.ActorName != req.ActorName { return false, true } if req.MethodName != "" && a.RPCMethod != req.MethodName { return false, true } if req.State != 0 { s, err := marshalActionState(a.State) if err != nil { return false, true } if s != req.State { return false, true } } return true, true } query := &firewalldb.ListActionsQuery{ IndexOffset: req.IndexOffset, MaxNum: req.MaxNumActions, Reversed: req.Reversed, CountAll: req.CountTotal, } var ( db = s.cfg.actionsDB actions []*firewalldb.Action lastIndex uint64 totalCount uint64 err error ) if req.SessionId != nil { sessionID, err := session.IDFromBytes(req.SessionId) if err != nil { return nil, err } actions, lastIndex, totalCount, err = db.ListSessionActions( sessionID, filterFn, query, ) if err != nil { return nil, err } } else { actions, lastIndex, totalCount, err = db.ListActions( filterFn, query, ) if err != nil { return nil, err } } resp := make([]*litrpc.Action, len(actions)) for i, a := range actions { state, err := marshalActionState(a.State) if err != nil { return nil, err } resp[i] = &litrpc.Action{ SessionId: a.SessionID[:], ActorName: a.ActorName, FeatureName: a.FeatureName, Trigger: a.Trigger, Intent: a.Intent, StructuredJsonData: a.StructuredJsonData, RpcMethod: a.RPCMethod, RpcParamsJson: string(a.RPCParamsJson), Timestamp: uint64(a.AttemptedAt.Unix()), State: state, ErrorReason: a.ErrorReason, } } return &litrpc.ListActionsResponse{ Actions: resp, LastIndexOffset: lastIndex, TotalCount: totalCount, }, nil } // ListAutopilotFeatures fetches all the features supported by the autopilot // server along with the rules that we need to support in order to subscribe // to those features. func (s *sessionRpcServer) ListAutopilotFeatures(ctx context.Context, _ *litrpc.ListAutopilotFeaturesRequest) ( *litrpc.ListAutopilotFeaturesResponse, error) { fs, err := s.cfg.autopilot.ListFeatures(ctx) if err != nil { return nil, err } features := make(map[string]*litrpc.Feature, len(fs)) for i, f := range fs { rules, upgrade, err := convertRules(s.cfg.ruleMgrs, f.Rules) if err != nil { return nil, err } features[i] = &litrpc.Feature{ Name: f.Name, Description: f.Description, Rules: rules, PermissionsList: marshalPerms(f.Permissions), RequiresUpgrade: upgrade, } } return &litrpc.ListAutopilotFeaturesResponse{ Features: features, }, nil } // AddAutopilotSession creates a new LNC session and attempts to register it // with the Autopilot server. func (s *sessionRpcServer) AddAutopilotSession(ctx context.Context, req *litrpc.AddAutopilotSessionRequest) ( *litrpc.AddAutopilotSessionResponse, error) { if len(req.Features) == 0 { return nil, fmt.Errorf("must include at least one feature") } expiry := time.Unix(int64(req.ExpiryTimestampSeconds), 0) if time.Now().After(expiry) { return nil, fmt.Errorf("expiry must be in the future") } privacy := !req.NoPrivacyMapper privacyMapPairs := make(map[string]string) // First need to fetch all the perms that need to be baked into this // mac based on the features. allFeatures, err := s.cfg.autopilot.ListFeatures(ctx) if err != nil { return nil, err } // Create lookup map of all the features that autopilot server offers. autopilotFeatureMap := make(map[string]*autopilotserver.Feature) for _, f := range allFeatures { autopilotFeatureMap[f.Name] = f } // allRules represents all the rules that our firewall knows about. allRules := s.cfg.ruleMgrs.GetAllRules() // Check that each requested feature is a valid autopilot feature and // that the necessary rules for the feature have been specified. featureRules := make(map[string]map[string]string, len(req.Features)) for f, rs := range req.Features { // Check that the features is known by the autopilot server. autopilotFeature, ok := autopilotFeatureMap[f] if !ok { return nil, fmt.Errorf("%s is not a features "+ "provided by the Autopilot server", f) } // reqRules is the rules specified in the request. var reqRules []rules.Values if rs.Rules != nil { reqRules = make([]rules.Values, 0, len(rs.Rules.Rules)) for ruleName, rule := range rs.Rules.Rules { v, err := s.cfg.ruleMgrs.UnmarshalRuleValues( ruleName, rule, ) if err != nil { return nil, err } if privacy { var privMapPairs map[string]string v, privMapPairs, err = v.RealToPseudo() if err != nil { return nil, err } for k, v := range privMapPairs { privacyMapPairs[k] = v } } reqRules = append(reqRules, v) } } // Create a lookup map for the rules specified in this feature. // Also check that each of the rules in the request is one known // to us. frs := make(map[string]rules.Values) for _, r := range reqRules { frs[r.RuleName()] = r _, ok := allRules[r.RuleName()] if !ok { return nil, fmt.Errorf("%s is not a known rule", r.RuleName()) } } // For each of the specified rules, we check that the values // set for those rules are sane given the bounds provided by // the autopilot server for the given feature. for _, r := range reqRules { ruleName := r.RuleName() autopilotSpecs, ok := autopilotFeature.Rules[ruleName] if !ok { return nil, fmt.Errorf("autopilot did not "+ "specify %s as a rule for feature %s", ruleName, autopilotFeature.Name) } min, err := s.cfg.ruleMgrs.InitRuleValues( ruleName, autopilotSpecs.MinVal, ) if err != nil { return nil, err } max, err := s.cfg.ruleMgrs.InitRuleValues( ruleName, autopilotSpecs.MaxVal, ) if err != nil { return nil, err } if err = r.VerifySane(min, max); err != nil { return nil, fmt.Errorf("rule value for %s "+ "not valid for feature %s. Expected "+ "rule value between %s and %s. Got "+ "%s. %v", ruleName, autopilotFeature.Name, min, max, r, err) } } // If the request did not contain specific values for a rule, // the default Autopilot rule value is used. finalRules := make( []rules.Values, 0, len(autopilotFeature.Rules), ) for name, values := range autopilotFeature.Rules { if r, ok := frs[name]; ok { finalRules = append(finalRules, r) continue } defaults, err := s.cfg.ruleMgrs.InitRuleValues( name, values.Default, ) if err != nil { return nil, err } finalRules = append(finalRules, defaults) } featureRules[f], err = marshalRulesToStringMap(finalRules) if err != nil { return nil, err } } interceptRules := &firewall.InterceptRules{ FeatureRules: featureRules, } // Gather all the permissions we need to add to the macaroon given the // feature list. var dedupedPerms = make(map[string]map[string]bool) for name, feature := range autopilotFeatureMap { if _, ok := req.Features[name]; !ok { continue } for _, ops := range feature.Permissions { for _, op := range ops { if dedupedPerms[op.Entity] == nil { dedupedPerms[op.Entity] = make( map[string]bool, ) } dedupedPerms[op.Entity][op.Action] = true } } } var perms []bakery.Op for entity, actions := range dedupedPerms { for action := range actions { perms = append(perms, bakery.Op{ Entity: entity, Action: action, }) } } rulesCaveatStr, err := firewall.RulesToCaveat(interceptRules) if err != nil { return nil, err } featureConfig := make(map[string][]byte, len(req.Features)) for name, f := range req.Features { featureConfig[name] = f.Config } caveats := []macaroon.Caveat{{Id: []byte(rulesCaveatStr)}} if privacy { caveats = append(caveats, firewall.MetaPrivacyCaveat) } sess, err := session.NewSession( req.Label, session.TypeAutopilot, expiry, req.MailboxServerAddr, req.DevServer, perms, caveats, featureConfig, privacy, ) if err != nil { return nil, fmt.Errorf("error creating new session: %v", err) } // Register all the privacy map pairs for this session ID. privDB := s.cfg.privMap(sess.ID) err = privDB.Update(func(tx firewalldb.PrivacyMapTx) error { for r, p := range privacyMapPairs { err := tx.NewPair(r, p) if err != nil { return err } } return nil }) if err != nil { return nil, err } // Attempt to register the session with the Autopilot server. remoteKey, err := s.cfg.autopilot.RegisterSession( ctx, sess.LocalPublicKey, sess.ServerAddr, sess.DevServer, featureConfig, ) if err != nil { return nil, fmt.Errorf("error registering session with "+ "autopilot server: %v", err) } // We only persist this session if we successfully retrieved the // autopilot's static key. sess.RemotePublicKey = remoteKey if err := s.db.StoreSession(sess); err != nil { return nil, fmt.Errorf("error storing session: %v", err) } if err := s.resumeSession(sess); err != nil { return nil, fmt.Errorf("error starting session: %v", err) } rpcSession, err := s.marshalRPCSession(sess) if err != nil { return nil, fmt.Errorf("error marshaling session: %v", err) } return &litrpc.AddAutopilotSessionResponse{ Session: rpcSession, }, nil } // ListAutopilotSessions fetches and returns all the sessions from the DB that // are of type TypeAutopilot. func (s *sessionRpcServer) ListAutopilotSessions(_ context.Context, _ *litrpc.ListAutopilotSessionsRequest) ( *litrpc.ListAutopilotSessionsResponse, error) { sessions, err := s.db.ListSessions(func(s *session.Session) bool { return s.Type == session.TypeAutopilot }) if err != nil { return nil, fmt.Errorf("error fetching sessions: %v", err) } response := &litrpc.ListAutopilotSessionsResponse{ Sessions: make([]*litrpc.Session, len(sessions)), } for idx, sess := range sessions { response.Sessions[idx], err = s.marshalRPCSession(sess) if err != nil { return nil, fmt.Errorf("error marshaling session: %v", err) } } return response, nil } // RevokeAutopilotSession revokes an autopilot session. func (s *sessionRpcServer) RevokeAutopilotSession(ctx context.Context, req *litrpc.RevokeAutopilotSessionRequest) ( *litrpc.RevokeAutopilotSessionResponse, error) { pubKey, err := btcec.ParsePubKey(req.LocalPublicKey) if err != nil { return nil, fmt.Errorf("error parsing public key: %v", err) } sess, err := s.db.GetSession(pubKey) if err != nil { return nil, err } if sess.Type != session.TypeAutopilot { return nil, session.ErrSessionNotFound } _, err = s.RevokeSession( ctx, &litrpc.RevokeSessionRequest{ LocalPublicKey: req.LocalPublicKey, }, ) if err != nil { return nil, err } return &litrpc.RevokeAutopilotSessionResponse{}, nil } func marshalRulesToStringMap(rs []rules.Values) (map[string]string, error) { res := make(map[string]string, len(rs)) for _, r := range rs { b, err := json.Marshal(r) if err != nil { return nil, err } res[r.RuleName()] = string(b) } return res, nil } // marshalPerms attempts to convert a set of permissions into their RPC // counterpart. If the list includes a rule that LiT does not know about, a nil // entry is included in the returned map. A bool is returned that indicates if // an upgrade is needed in order to enforce an unknown rule. func convertRules(ruleMgr rules.ManagerSet, ruleList map[string]*autopilotserver.RuleValues) ( map[string]*litrpc.RuleValues, bool, error) { var ( upgrade bool res = make( map[string]*litrpc.RuleValues, len(ruleList), ) knownRules = ruleMgr.GetAllRules() ) for name, rule := range ruleList { known := true if !knownRules[name] { upgrade = true known = false } defaultVals, err := ruleMgr.InitRuleValues(name, rule.Default) if err != nil { return nil, false, err } minVals, err := ruleMgr.InitRuleValues(name, rule.MinVal) if err != nil { return nil, false, err } maxVals, err := ruleMgr.InitRuleValues(name, rule.MaxVal) if err != nil { return nil, false, err } res[name] = &litrpc.RuleValues{ Known: known, Defaults: defaultVals.ToProto(), MinValue: minVals.ToProto(), MaxValue: maxVals.ToProto(), } } return res, upgrade, nil } // marshalPerms converts a set of permissions into their RPC counterpart. func marshalPerms(perms map[string][]bakery.Op) []*litrpc.Permissions { var res []*litrpc.Permissions for method, ops := range perms { rpcOps := make([]*litrpc.MacaroonPermission, len(ops)) for i, op := range ops { rpcOps[i] = &litrpc.MacaroonPermission{ Entity: op.Entity, Action: op.Action, } } res = append(res, &litrpc.Permissions{ Method: method, Operations: rpcOps, }) } return res } // marshalRPCSession converts a session into its RPC counterpart. func (s *sessionRpcServer) marshalRPCSession(sess *session.Session) ( *litrpc.Session, error) { rpcState, err := marshalRPCState(sess.State) if err != nil { return nil, err } rpcType, err := marshalRPCType(sess.Type) if err != nil { return nil, err } var remotePubKey []byte if sess.RemotePublicKey != nil { remotePubKey = sess.RemotePublicKey.SerializeCompressed() } mnemonic, err := mailbox.PassphraseEntropyToMnemonic(sess.PairingSecret) if err != nil { return nil, err } macRecipe := marshalRPCMacaroonRecipe(sess.MacaroonRecipe) var revokedAt uint64 if !sess.RevokedAt.IsZero() { revokedAt = uint64(sess.RevokedAt.Unix()) } featureInfo := make(map[string]*litrpc.RulesMap) if sess.MacaroonRecipe != nil { for _, cav := range sess.MacaroonRecipe.Caveats { info, err := firewall.ParseRuleCaveat(string(cav.Id)) if errors.Is(err, firewall.ErrNoRulesCaveat) { continue } else if err != nil { return nil, err } for feature, rules := range info.FeatureRules { ruleMap := make(map[string]*litrpc.RuleValue) for name, rule := range rules { val, err := s.cfg.ruleMgrs.InitRuleValues(name, []byte(rule)) if err != nil { return nil, err } if sess.WithPrivacyMapper { db := s.cfg.privMap(sess.ID) val, err = val.PseudoToReal(db) if err != nil { return nil, err } } ruleMap[name] = val.ToProto() } featureInfo[feature] = &litrpc.RulesMap{Rules: ruleMap} } } } return &litrpc.Session{ Id: sess.ID[:], Label: sess.Label, SessionState: rpcState, SessionType: rpcType, ExpiryTimestampSeconds: uint64(sess.Expiry.Unix()), MailboxServerAddr: sess.ServerAddr, DevServer: sess.DevServer, PairingSecret: sess.PairingSecret[:], PairingSecretMnemonic: strings.Join(mnemonic[:], " "), LocalPublicKey: sess.LocalPublicKey.SerializeCompressed(), RemotePublicKey: remotePubKey, CreatedAt: uint64(sess.CreatedAt.Unix()), RevokedAt: revokedAt, MacaroonRecipe: macRecipe, AutopilotFeatureInfo: featureInfo, }, nil } // marshalRPCMacaroonRecipe converts a macaroon recipe (permissions and caveats) // into its RPC counterpart. func marshalRPCMacaroonRecipe( recipe *session.MacaroonRecipe) *litrpc.MacaroonRecipe { if recipe == nil { return nil } perms := make([]*litrpc.MacaroonPermission, len(recipe.Permissions)) for i, op := range recipe.Permissions { perms[i] = &litrpc.MacaroonPermission{ Entity: op.Entity, Action: op.Action, } } caveats := make([]string, len(recipe.Caveats)) for i, cav := range recipe.Caveats { caveats[i] = string(cav.Id) } return &litrpc.MacaroonRecipe{ Permissions: perms, Caveats: caveats, } } // marshalRPCState converts a session state to its RPC counterpart. func marshalRPCState(state session.State) (litrpc.SessionState, error) { switch state { case session.StateCreated: return litrpc.SessionState_STATE_CREATED, nil case session.StateInUse: return litrpc.SessionState_STATE_IN_USE, nil case session.StateRevoked: return litrpc.SessionState_STATE_REVOKED, nil case session.StateExpired: return litrpc.SessionState_STATE_EXPIRED, nil default: return 0, fmt.Errorf("unknown state <%d>", state) } } // marshalRPCType converts a session type to its RPC counterpart. func marshalRPCType(typ session.Type) (litrpc.SessionType, error) { switch typ { case session.TypeMacaroonReadonly: return litrpc.SessionType_TYPE_MACAROON_READONLY, nil case session.TypeMacaroonAdmin: return litrpc.SessionType_TYPE_MACAROON_ADMIN, nil case session.TypeMacaroonCustom: return litrpc.SessionType_TYPE_MACAROON_CUSTOM, nil case session.TypeUIPassword: return litrpc.SessionType_TYPE_UI_PASSWORD, nil case session.TypeMacaroonAccount: return litrpc.SessionType_TYPE_MACAROON_ACCOUNT, nil case session.TypeAutopilot: return litrpc.SessionType_TYPE_AUTOPILOT, nil default: return 0, fmt.Errorf("unknown type <%d>", typ) } } // unmarshalRPCType converts an RPC session type to its session counterpart. func unmarshalRPCType(typ litrpc.SessionType) (session.Type, error) { switch typ { case litrpc.SessionType_TYPE_MACAROON_READONLY: return session.TypeMacaroonReadonly, nil case litrpc.SessionType_TYPE_MACAROON_ADMIN: return session.TypeMacaroonAdmin, nil case litrpc.SessionType_TYPE_MACAROON_CUSTOM: return session.TypeMacaroonCustom, nil case litrpc.SessionType_TYPE_UI_PASSWORD: return session.TypeUIPassword, nil case litrpc.SessionType_TYPE_MACAROON_ACCOUNT: return session.TypeMacaroonAccount, nil case litrpc.SessionType_TYPE_AUTOPILOT: return session.TypeAutopilot, nil default: return 0, fmt.Errorf("unknown type <%d>", typ) } } // marshalActionState converts an Action state into its RPC counterpart. func marshalActionState(state firewalldb.ActionState) (litrpc.ActionState, error) { switch state { case firewalldb.ActionStateUnknown: return litrpc.ActionState_STATE_UNKNOWN, nil case firewalldb.ActionStateInit: return litrpc.ActionState_STATE_PENDING, nil case firewalldb.ActionStateDone: return litrpc.ActionState_STATE_DONE, nil case firewalldb.ActionStateError: return litrpc.ActionState_STATE_ERROR, nil default: return 0, fmt.Errorf("unknown state <%d>", state) } }