package modules import ( "context" "encoding/json" "fmt" "os" "strings" "git.frostfs.info/TrueCloudLab/frostfs-contract/commonclient" "git.frostfs.info/TrueCloudLab/frostfs-contract/policy" ffsidContract "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid/contract" policyContact "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy/contract" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" ) var registerUserCmd = &cobra.Command{ Use: "register-user", Short: "Register user and add allowed policy to him", Long: "Register user in FrostFSID contract and add allowed policies. This is need to get access to s3-gw operations.", Example: `frostfs-s3-authmate register-user --wallet wallet.json --rpc-endpoint http://morph-chain.frostfs.devenv:30333 frostfs-s3-authmate register-user --wallet wallet.json --contract-wallet contract-wallet.json --rpc-endpoint http://morph-chain.frostfs.devenv:30333 --namespace namespace --frostfsid-name devenv --frostfsid-contract frostfsid.frostfs --proxy-contract proxy.frostfs --policy-contract policy.frostfs`, RunE: runRegisterUserCmd, } const ( frostfsIDContractFlag = "frostfsid-contract" proxyContractFlag = "proxy-contract" usernameFlag = "username" namespaceFlag = "namespace" policyContractFlag = "policy-contract" contractWalletFlag = "contract-wallet" contractWalletAddressFlag = "contract-wallet-address" rpcEndpointFlag = "rpc-endpoint" ) const walletContractPassphraseCfg = "wallet.contract.passphrase" func initRegisterUserCmd() { registerUserCmd.Flags().String(walletFlag, "", "Path to the wallet with account of the user that will be registered in FrostFS ID contract") registerUserCmd.Flags().String(addressFlag, "", "Address of the user wallet that will be registered in FrostFS ID contract") registerUserCmd.Flags().String(frostfsIDContractFlag, "frostfsid.frostfs", "FrostfsID contract hash (LE) or name in NNS to register public key in contract") registerUserCmd.Flags().String(proxyContractFlag, "proxy.frostfs", "Proxy contract hash (LE) or name in NNS to use when interact with frostfsid contract") registerUserCmd.Flags().String(namespaceFlag, "", "Namespace to register public key in frostfsid contract and add policy chains") registerUserCmd.Flags().String(usernameFlag, "", "Username to set for public key in frostfsid contract") registerUserCmd.Flags().String(contractWalletFlag, "", "Path to wallet that will be used to interact with contracts (if missing key from wallet flag be used)") registerUserCmd.Flags().String(contractWalletAddressFlag, "", "Address of the contract wallet account") registerUserCmd.Flags().String(policyContractFlag, "policy.frostfs", "Policy contract hash (LE) or name in NNS to save allowed chains for key") registerUserCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address") _ = registerUserCmd.MarkFlagRequired(walletFlag) _ = registerUserCmd.MarkFlagRequired(rpcEndpointFlag) } func runRegisterUserCmd(cmd *cobra.Command, _ []string) error { ctx, cancel := context.WithTimeout(cmd.Context(), viper.GetDuration(timeoutFlag)) defer cancel() log := getLogger() key, contractKey, err := parseKeys() if err != nil { return wrapPreparationError(err) } frostfsIDClient, err := initFrostFSIDContract(ctx, log, contractKey) if err != nil { return wrapFrostFSIDInitError(err) } if err = registerPublicKey(log, frostfsIDClient, key.PublicKey()); err != nil { return wrapBusinessLogicError(err) } policyClient, err := initPolicyContract(ctx, log, contractKey) if err != nil { return wrapPolicyInitError(err) } if err = addAllowedPolicyChains(cmd, log, policyClient, key.PublicKey()); err != nil { return wrapBusinessLogicError(err) } return nil } func parseKeys() (userKey *keys.PrivateKey, contractKey *keys.PrivateKey, err error) { password := wallet.GetPassword(viper.GetViper(), walletPassphraseCfg) key, err := wallet.GetKeyFromPath(viper.GetString(walletFlag), viper.GetString(addressFlag), password) if err != nil { return nil, nil, fmt.Errorf("failed to load frostfs private key: %s", err) } contractKey = key if contractWallet := viper.GetString(contractWalletFlag); contractWallet != "" { password = wallet.GetPassword(viper.GetViper(), walletContractPassphraseCfg) contractKey, err = wallet.GetKeyFromPath(contractWallet, viper.GetString(contractWalletAddressFlag), password) if err != nil { return nil, nil, fmt.Errorf("failed to load contract private key: %s", err) } } return key, contractKey, nil } func initFrostFSIDContract(ctx context.Context, log *zap.Logger, key *keys.PrivateKey) (*ffsidContract.FrostFSID, error) { log.Debug(logs.PrepareFrostfsIDClient) cfg := ffsidContract.Config{ RPCAddress: viper.GetString(rpcEndpointFlag), Contract: viper.GetString(frostfsIDContractFlag), ProxyContract: viper.GetString(proxyContractFlag), Key: key, Waiter: commonclient.WaiterOptions{ IgnoreAlreadyExistsError: false, VerifyExecResults: true, }, } cli, err := ffsidContract.New(ctx, cfg) if err != nil { return nil, fmt.Errorf("create frostfsid client: %w", err) } return cli, nil } func registerPublicKey(log *zap.Logger, cli *ffsidContract.FrostFSID, key *keys.PublicKey) error { namespace := viper.GetString(namespaceFlag) log.Debug(logs.CreateSubjectInFrostFSID) err := cli.Wait(cli.CreateSubject(namespace, key)) if err != nil { if strings.Contains(err.Error(), "subject already exists") { log.Debug(logs.SubjectAlreadyExistsInFrostFSID, zap.String("address", key.Address())) } else { return fmt.Errorf("create subject in frostfsid: %w", err) } } name := viper.GetString(usernameFlag) if name == "" { return nil } log.Debug(logs.SetSubjectNameInFrostFSID) if err = cli.Wait(cli.SetSubjectName(key, name)); err != nil { return fmt.Errorf("set subject name in frostfsid: %w", err) } return nil } func initPolicyContract(ctx context.Context, log *zap.Logger, key *keys.PrivateKey) (*policyContact.Client, error) { log.Debug(logs.PreparePolicyClient) cfg := policyContact.Config{ RPCAddress: viper.GetString(rpcEndpointFlag), Contract: viper.GetString(policyContractFlag), ProxyContract: viper.GetString(proxyContractFlag), Key: key, } cli, err := policyContact.New(ctx, cfg) if err != nil { return nil, fmt.Errorf("create policy client: %w", err) } return cli, nil } func addAllowedPolicyChains(cmd *cobra.Command, log *zap.Logger, cli *policyContact.Client, key *keys.PublicKey) error { log.Debug(logs.AddPolicyChainRules) namespace := viper.GetString(namespaceFlag) allowAllRule := chain.Rule{ Status: chain.Allow, Actions: chain.Actions{Names: []string{"*"}}, Resources: chain.Resources{Names: []string{"*"}}, } chains := []*chain.Chain{ {ID: chain.ID(chain.S3 + ":authmate"), Rules: []chain.Rule{allowAllRule}}, {ID: chain.ID(chain.Ingress + ":authmate"), Rules: []chain.Rule{allowAllRule}}, } kind := policy.Kind(policy.User) entity := namespace + ":" + key.Address() tx := cli.StartTx() for _, ch := range chains { tx.AddChain(kind, entity, ch.ID, ch.Bytes()) } if err := cli.SendTx(tx); err != nil { return fmt.Errorf("add policy chain: %w", err) } cmd.Printf("Added policy rules:\nkind: '%c'\nentity: '%s'\nchains:\n", kind, entity) enc := json.NewEncoder(os.Stdout) return enc.Encode(chains) }