Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass Backend #62

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

Pass Backend #62

wants to merge 2 commits into from

Conversation

MagicRB
Copy link
Contributor

@MagicRB MagicRB commented Apr 5, 2022

Signed-off-by: Magic_RB [email protected]

Description

Add a pass backend calling the pass CLI program directly. As discussed in #55

Related issue(s)

Directly discusses #55.

✅ Checklist for your Pull Request

Related changes (conditional)

  • Tests
    • If I added new functionality, I added tests covering it.
    • If I fixed a bug, I added a regression test to prevent the bug from
      silently reappearing again.
  • Documentation
    • I checked whether I should update the docs and did so if necessary:
  • Public contracts
    • Any modifications of public contracts comply with the Evolution
      of Public Contracts
      policy.
    • I added an entry to the changelog if my changes are visible to the users
      and
    • provided a migration guide for breaking changes if possible

Stylistic guide (mandatory)

@MagicRB MagicRB changed the title WIP Pass Backend Apr 18, 2022
@MagicRB MagicRB force-pushed the magic_rb/#55-pass-backend branch 4 times, most recently from 6432a1d to 52de877 Compare April 18, 2022 20:21
@MagicRB
Copy link
Contributor Author

MagicRB commented Apr 18, 2022

Ok, think that's. So this is a weird one, or rather weird 3. It's actually 3 PRs in one. I'll split them up before merge, but we can review them now together, since they all developed together, triplets so to speak. Anyway. PR list:

  1. the actual backend, Backend/Pass.hs and Entry/Pass.hs
  2. the supporting debug backend, very useful thing, Backend/Debug.hs
  3. and finally an experiment. I needed a nice FS api and i wanted to try something more type bound, it's not as special as i wanted but it works and we can use it instead of IO, Effect/Fs.hs

Ok, so that's all. The last part I'm not happy with is Entry/Pass.hs, it's a parsing spaghetti hell as is common with marshalling code, but that in particular is hellish. I'm not really certain what to do with it, the format itself is a little thrown together since we're trying to fit into pass' standards. But, actually, it's similar to Vault, so maybe we could unify those formats. The main issue with the code right now, is the "modern art" of chaining functions and making little "pipelines". That would be solved by breaking the pipeline up into parts and giving those parts meaningful names. That's all.

@MagicRB MagicRB marked this pull request as ready for review April 18, 2022 20:37
@MagicRB
Copy link
Contributor Author

MagicRB commented Apr 18, 2022

Oh and I have no idea why it doesn't build. None at all, it builds on my machine:tm:

@MagicRB MagicRB force-pushed the magic_rb/#55-pass-backend branch from 52de877 to 77286c3 Compare April 18, 2022 20:38

data DebugBackend =
DebugBackend
{ dSubType :: T.Text
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have explicit Text import, so you don't need to write T.Text.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought i got them, Ill grep for any T.Text and BS.Bytestring stragglers

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be all of them, i think

lib/Backend/Debug.hs Outdated Show resolved Hide resolved
lib/Backend/Debug.hs Outdated Show resolved Hide resolved
lib/Backend/Debug.hs Outdated Show resolved Hide resolved
lib/Backend/Debug.hs Outdated Show resolved Hide resolved
lib/Effect/Fs.hs Outdated
Comment on lines 161 to 160
_listDirectoryRec dirPath =
_listDirectory dirPath
>>= mapM \case Left f -> pure $ NodeRec $ Left f
Right d -> _listDirectoryRec d
<&> NodeRec . Right . Directory
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, use do-notation + forM

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to BlockArguments you can write smth like this

_listDirectoryRec dirPath = do
  list <- _listDirectory dirPath
  forM list \case
    Left f -> pure $ NodeRec $ Left f
    Right d ->
      _listDirectoryRec d <&> NodeRec . Right . Directory

Comment on lines 50 to 53
rightToMaybe :: Either a b -> Maybe b
rightToMaybe = \case
Left _ -> Nothing
Right b -> Just b
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

pure $ E.newEntry entryPath dateModified
& E.fields .~ fields
& E.tags .~ tags
& E.masterField .~ (masterField >>= rightToMaybe . E.newFieldKey)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're losing information about errors here (and in some other places).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't really get errors out, since prisms don't support errors and all. That's my fault, it needs a refactor.

lib/Backend/Pass.hs Outdated Show resolved Hide resolved
Comment on lines 206 to 221
let parseLine
:: Maybe String
-> Parser T.Text
parseLine label =
do
x <- takeWhileP label (/='\n')
try newline
pure x
let parsePair
:: Parser (T.Text, T.Text)
parsePair = do
key <- takeWhileP (Just "key") (/='=')
char '='
value <- takeWhileP (Just "value") (/='\n')
char '\n' <|> pure '\n'
pure (key, value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these functions could be in where block

@MagicRB MagicRB force-pushed the magic_rb/#55-pass-backend branch from 77286c3 to 304691f Compare April 20, 2022 05:34
@MagicRB
Copy link
Contributor Author

MagicRB commented Apr 20, 2022

I've gone through most, thanks for the input. I've noticed that mostly you see more combinators that could be used to make the code simpler. I guess that comes with experience.

@MagicRB MagicRB force-pushed the magic_rb/#55-pass-backend branch from 304691f to da7170b Compare April 20, 2022 05:37
Signed-off-by: Magic_RB <[email protected]>
@MagicRB MagicRB force-pushed the magic_rb/#55-pass-backend branch from da7170b to 32e0702 Compare April 21, 2022 16:36
Copy link
Member

@DK318 DK318 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, don't squash your commits after review. It's really hard to rereview without seeing diff after the first review

@@ -2,11 +2,11 @@
#
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, remember to rollback this config when you are done

Comment on lines +52 to +53


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to styleguide we must add one blank line between top-level definitions.

lib/Effect/Fs.hs Outdated
Comment on lines 61 to 62
Right b -> Right $ T.unpack b
stringToPath :: String -> Path
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

@dcastro
Copy link
Member

dcastro commented Apr 24, 2022

Oh and I have no idea why it doesn't build. None at all, it builds on my machine

@MagicRB It's because you edited the coffer.cabal file directly.

We've since added support for hpack + stack, so now we should edit the package.yaml file instead. And then use ./scripts/generate-cabal-files.sh to update coffer.cabal, cabal.project, cabal.project.freeze, etc.

We have a CI step to validate that the stack and cabal files are in sync (see here), but the check seems to be faulty... the step succeeded in this PR's pipeline and it shouldn't 🤔 I'll look into this.

@dcastro
Copy link
Member

dcastro commented Apr 24, 2022

We have a CI step to validate that the stack and cabal files are in sync (see here), but the check seems to be faulty... the step succeeded in this PR's pipeline and it shouldn't 🤔 I'll look into this.

Ok, found the issue, fixed it here: #77

Left e -> throw $ OtherError (show e & T.pack)
Right (Just _) -> pure ()
Right Nothing -> throw . OtherError $
"You must first initialize the password store at: " <> T.pack storeDir
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, that's a good check! 👏

Comment on lines 49 to 51
where tToFPath = Just . T.unpack
fPathToT :: Maybe String -> Maybe Text
fPathToT a = a <&> T.pack
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the indentation to a minimum.

From the styleguide:

Indent the where keyword with 2 spaces and the definitions within the
where clause with 2 more spaces

  where
    tToFPath = Just . T.unpack
    fPathToT :: Maybe String -> Maybe Text
    fPathToT a = a <&> T.pack

Please look for other similar cases (e.g. in debugCodec, pbListSecrets, etc)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think i got them all, ill regex it, yup got them all

lib/Backend/Pass.hs Show resolved Hide resolved
PassBackend
<$> backendNameCodec "name" Toml..= pbName
<*> Toml.string "store_dir" Toml..= pbStoreDir
<*> Toml.dimatch fPathToT tToFPath (Toml.text "pass_exe") Toml..= pbPassExe
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using Toml.text and then mapping to/contramapping from String, we can just use Toml.string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

>>= (\case Left e ->
if | isDoesNotExistError e -> pure Nothing
| True -> throw $ OtherError (T.pack $ show e)
Right v -> pure $ Just v)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use descriptive variable names instead of v/a/etc.

From the code style:

You should not use short names like n, sk, f, unless their meaning is
clear from the context (function name, types, other variables, etc.).

lib/Effect/Fs.hs Outdated
pathToString :: Path -> Either FsError String
pathToString path =
case decodeUtf8' path of
Left a -> Left $ FEInvalidPath path
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the UnicodeException in the Left should also be included in FEInvalidPath

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved


type Node f d = Either (File f) (Directory d)
type Node' a = Node a a
type Path = ByteString
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use the FilePath alias from the prelude?

I get that technically a unix filepath can be any byte sequence, it doesn't have to be unicode. And the prelude's type FilePath = String assumes the path only contains unicode characters.

So type Path = ByteString is more correct. However, we're internally converting those bytestrings to strings anyway (with pathToString), so we might as well just use FilePath from the prelude.

deriving stock (Show)
makeLensesWith abbreviatedFields ''PassEntry

passFieldPrism :: Prism' PassField (E.FieldKey, E.Field)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like we talked on Slack, prisms shouldn't really be used for parsing because it's impossible to know why parsing failed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment below.

deriving stock (Show)
makeLensesWith abbreviatedFields ''PassField

data PassEntry =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these intermediate data structures needed?

In order to write an Entry to pass, we're converting Entry -> PassEntry -> PassKv -> Text

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I plan to remedy that globally, in the Prism rework. I want to merge Vault's format and pass' format. They're both KV, just pass uses a flat structure, vault a nested one. Names are different but that can be generalized too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I plan to remedy that globally, in the Prism rework

Can you please create an issue for that? Please detail what will be done.

I want to merge Vault's format and pass' format. They're both KV, just pass uses a flat structure, vault a nested one. Names are different but that can be generalized too.

Can you expand a bit on this?
If I'm understanding correctly, that would justify the need for PassKv, but I still don't see the need for the PassEntry intermediate data structure (I haven't looked at this part of the code in detail yet, so I might just be missing something).

Copy link
Contributor Author

@MagicRB MagicRB Apr 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm understanding correctly, that would justify the need for PassKv, but I still don't see the need for the PassEntry intermediate data structure (I haven't looked at this part of the code in detail yet, so I might just be missing something).

PassEntry exists, because the code is ugly and confusing and I thought that if I was trying to convert from a to Text and stuff into HashMap Text Text the code would be completely uncomprehensible.

Can you please create an issue for that? Please detail what will be done.

sure

lib/Effect/Fs.hs Outdated Show resolved Hide resolved
@MagicRB
Copy link
Contributor Author

MagicRB commented Apr 25, 2022

#62 (comment) idk why but i cant seem to reply to this, so doing it here. stringToPath is fine, we're not doing pathToString anywhere, which would be an issue.

lib/Effect/Fs.hs Outdated

data FsEffect m a where
NodeExists :: Path -> FsEffect m (Maybe (Node' ()))
GetNode :: Path -> FsEffect m (Maybe (Node' Path))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_getNode doesn't seem to be a "primitive operation", it's more like a "helper" wrapper over _nodeExists. So I don't think we need to have it here, as a constructor of FsEffect. We can define it simply as:

getNode :: Member FsEffect r => Path -> Sem r (Maybe (Node' Path))
getNode path = do
  mNode <- nodeExists path
  pure
    $ mNode <&> bimap
        (const (File path))
        (const (Directory path))

Same thing for _listDirectoryRec, it's just a higher-level helper that builds on top of _listDirectory

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, getNode and listDirectoryRec aren't used anywhere, so we should delete them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getNode returning Path is actually an error on my part, i just fit the types together. I meant for it to return a ByteString or a [Path] depending on whether it's a dir or file, but I'll yeet it. The idea keeping these in was to build up a small library that I planned to factor out in my own free time.

lib/Effect/Fs.hs Outdated
data FsEffect m a where
NodeExists :: Path -> FsEffect m (Maybe (Node' ()))
GetNode :: Path -> FsEffect m (Maybe (Node' Path))
ListDirectory :: Directory Path -> FsEffect m [Node' ByteString]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't ListDirectory return a Maybe, and Nothing when the given path doesn't exist?

verifyPassStore storeDir

let fpath = storeDir <> (path & build & fmt)
contents <- runError (fromException @IOException $ D.listDirectory fpath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we be using listDirectory from the FsEffect here?


case exitCode of
ExitSuccess ->
pure $ T.decodeUtf8 (BS.toStrict stdout) ^? passTextPrism . E.entry
Copy link
Member

@dcastro dcastro Apr 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, if decoding fails, we're returning Nothing.

But that's wrong - Nothing, in this context, means that the entry doesn't exist.

If it does exist but, for some reason, parsing failed, then we should throw an error.


case exitCode of
ExitSuccess -> pure ()
ExitFailure _e -> throw $ OtherError (T.decodeUtf8 $ BS.toStrict stderr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_e

We shouldn't swallow the exit code, we should report that as well.

Same thing in pbWriteSecret and pbReadSecret

--
-- SPDX-License-Identifier: MPL-2.0

module Backend.Debug
Copy link
Member

@dcastro dcastro Apr 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I agree with this approach...

It's essentially a very limited type of logging that can only log the input/output of BackendEffect operations. It cannot log what's happening inside the BackendEffect implementation, it can't log what's happening in the high-level Commands module, etc.

IMO, we should have a --verbose/-v switch that turns on logging throughout the entire codebase, wherever it may be useful. Plus, this wouldn't require the user to change their config file.

We could have a LogEffect and two interpreters: one that logs to stdout and another that does nothing. In main, depending on whether the -v switch was used, we choose which interpreter to use.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be enabled easily by any other method, i wrote the debug backend because i was going nuts over not understanding what happening. We can reuse the impl later for a proper -v mode

-- ReadNode nodePath -> undefined
-- CreateNode nodePath -> undefined

_nodeExists
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't prefix function names with _, it has a special meaning to GHC.

Prefixing with _ disables GHC's "unused definition" warnings/errors.

@@ -22,6 +22,8 @@ library
Backend
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a battery of integration/golden tests in tests/golden.
We should run those tests against the pass backend as well.

Those tests are run by make bats, which in turn calls the scripts/run-bats-tests.sh script.
At the moment, that script:

  1. spins up 2 vault instances
  2. runs the bats tests
  3. kills the 2 vault instances

We should modify the script such that:

  1. sets up 2 pass instances
  2. exports COFFER_CONFIG="pass-config.toml"
  3. runs the tests
  4. cleans up the 2 pass instances
  5. spins up 2 vault instances
  6. exports COFFER_CONFIG="vault-config.toml"
  7. runs the tests again
  8. kills the 2 vault instances

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've found 2 bugs so far:


Creating an entry with a multiline field content succeeds (as in, it creates a file /tmp/pass-store/dir/entry4 on disk), but then coffer view says it doesn't exist.

$ coffer create /dir/entry4 --field "user=$(echo -e "first\nsecond")"
[SUCCESS] Entry created at '/dir/entry4'.

$ coffer view /dir                                                   
[ERROR] Entry or directory not found at '/dir'.

Haven't looked too much into it, but I suspect the parsing is failing. And the parser failure is being masked by the other bug I mentioned in another comment; we're returning Nothing in pbReadSecret when decoding fails.


This may or may not be a bug, maybe I'm doing something wrong? I don't know. But setting up a new pass instance and then running coffer / fails for me. Running coffer view with any path other than / works though:

$ export PASSWORD_STORE_DIR='/tmp/pass-store'

$ pass init [email protected]
mkdir: created directory '/tmp/pass-store/'
Password store initialized for [email protected]

$ coffer /
ListSecrets: Path {unPath = []}
out: Just [".gp"]
OtherError "Internal error:\nBackend returned a secret that is not a valid entry or directory name.\nGot: '.gp'.\n"

Signed-off-by: Magic_RB <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants