Skip to content

Commit ebdfcbe

Browse files
committed
add support for token auth for Hackage
1 parent 517e30a commit ebdfcbe

File tree

13 files changed

+137
-70
lines changed

13 files changed

+137
-70
lines changed

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* Add the options `envs.*.ghci{,d}.args` as env-specific extra arguments to those commands, based on env selection.
2020
* Add the options `ghci{,d}.args` as unconditional extra arguments to those commands.
2121
* Add the CLI option `--env` to override env selection for commands.
22+
* Add support for token authentication for Hackage.
2223

2324
# 0.9.0
2425

lib/doc/prose.nix

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,8 +1650,7 @@ in {
16501650
16511651
# Default for `publish` is `true` for central Hackage
16521652
"hackage.haskell.org" = {
1653-
user = "deepspace-mining-corp";
1654-
password = {
1653+
token = {
16551654
type = "exec";
16561655
value = "/path/to/password/script";
16571656
};
@@ -1676,8 +1675,8 @@ in {
16761675
}
16771676
```
16781677
1679-
With this config, the app will execute the configured script to obtain the password for central Hackage, and fetch
1680-
that for `prod` from the given environment variable.
1678+
With this config, the app will execute the configured script to obtain the auth token for central Hackage, and fetch
1679+
the password for `prod` from the given environment variable.
16811680
For `staging`, the credentials must be specified as CLI args:
16821681
16831682
```

modules/hackage-repo.nix

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ in {
4848
default = null;
4949
};
5050

51+
token = util.maybeOption types.str {
52+
description = ''
53+
Authentication token for uploading.
54+
'';
55+
};
56+
5157
secure = lib.mkOption {
5258
description = "Use the newer Cabal client that verifies index signatures via `hackage-security`.";
5359
type = types.nullOr types.bool;

packages/hix/lib/Hix/Managed/Cabal/Config.hs

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,18 @@ import Hix.Managed.Cabal.Data.ContextHackageRepo (
2828
ContextHackageLocation (..),
2929
ContextHackagePassword (..),
3030
ContextHackageRepo (..),
31+
ContextHackageSecret (..),
32+
ContextHackageToken (..),
3133
)
3234
import qualified Hix.Managed.Cabal.Data.HackageLocation as HackageLocation
33-
import Hix.Managed.Cabal.Data.HackageLocation (HackageLocation (auth), HackagePassword (HackagePassword), HackageUser)
35+
import Hix.Managed.Cabal.Data.HackageLocation (
36+
HackageAuth (..),
37+
HackageLocation (auth),
38+
HackagePassword (..),
39+
HackageSecret (..),
40+
HackageToken (..),
41+
HackageUser,
42+
)
3443
import Hix.Managed.Cabal.Data.HackageRepo (HackageName, HackageRepo (..), centralName)
3544
import Hix.Managed.Cabal.HackageLocation (parseLocation)
3645
import Hix.Managed.Cabal.HackageRepo (hackageDescription)
@@ -64,18 +73,18 @@ isReinstallableId package = isReinstallable package.name
6473
isNonReinstallableDep :: MutableDep -> Bool
6574
isNonReinstallableDep = isNonReinstallable . depName
6675

67-
resolvePasswordEnvVar :: Text -> M HackagePassword
68-
resolvePasswordEnvVar name =
76+
resolveSecretEnvVar :: Text -> M HackageSecret
77+
resolveSecretEnvVar name =
6978
lookup >>= \case
7079
[] -> clientError (message "is empty")
71-
value -> pure (HackagePassword (toText value))
80+
value -> pure (HackageSecret (toText value))
7281
where
7382
lookup = noteClient (message "does not exist") =<< tryIOM (lookupEnv (toString name))
7483

7584
message problem = [exon|The specified environment variable #{Color.cyan name} #{problem}|]
7685

77-
resolvePasswordExec :: Text -> M HackagePassword
78-
resolvePasswordExec spec =
86+
resolveSecretExec :: Text -> M HackageSecret
87+
resolveSecretExec spec =
7988
noteClient (message "is not a valid path") (Path.parseSomeFile (toString spec)) >>= \case
8089
Abs path -> checkPath path
8190
Rel rel ->
@@ -93,7 +102,7 @@ resolvePasswordExec spec =
93102
liftIO (tryIOError (Process.readProcessStdout (Process.proc (toFilePath path) []))) >>= \case
94103
Right (ExitSuccess, output)
95104
| LByteString.null output -> failure "printed nothing on stdout"
96-
| [pw] <- Text.lines (decodeUtf8 output) -> pure (HackagePassword pw)
105+
| [secret] <- Text.lines (decodeUtf8 output) -> pure (HackageSecret secret)
97106
| otherwise -> failure "printed multiple lines"
98107
Right (ExitFailure code, _) -> failure [exon|exited with code #{show @_ @Int code}|]
99108
Left err -> do
@@ -104,40 +113,48 @@ resolvePasswordExec spec =
104113

105114
message problem = [exon|The specified executable #{Color.path spec} #{problem}|]
106115

107-
resolvePassword :: ContextHackagePassword -> M HackagePassword
108-
resolvePassword =
116+
resolveSecret :: ContextHackageSecret -> M HackageSecret
117+
resolveSecret =
109118
appContextVerbose ctx . \case
110-
PasswordUnobscured pw -> pure pw
111-
PasswordPlain pw -> pure pw
112-
PasswordEnvVar name -> resolvePasswordEnvVar name
113-
PasswordExec path -> resolvePasswordExec path
119+
SecretUnobscured secret -> pure secret
120+
SecretPlain secret -> pure secret
121+
SecretEnvVar name -> resolveSecretEnvVar name
122+
SecretExec path -> resolveSecretExec path
114123
where
115124
ctx = "resolving the password"
116125

117126
withAuth ::
118127
HackageLocation ->
119128
Maybe HackageUser ->
120129
Maybe ContextHackagePassword ->
130+
Maybe ContextHackageToken ->
121131
M HackageLocation
122132
withAuth location = \cases
123-
(Just user) Nothing ->
133+
_ (Just _) (Just _) ->
134+
bothSorts
135+
(Just user) Nothing _ ->
124136
onlyOne [exon|user (##{user})|] "password"
125-
Nothing (Just _) ->
137+
Nothing (Just _) Nothing ->
126138
onlyOne "password" "user"
127-
Nothing Nothing ->
139+
Nothing Nothing Nothing ->
128140
pure location
129-
(Just user) (Just passwordSpec) -> do
130-
password <- resolvePassword passwordSpec
131-
pure location {auth = Just (user, password)}
141+
(Just user) (Just passwordSpec) Nothing -> do
142+
secret <- resolveSecret passwordSpec.secret
143+
pure location {auth = Just (HackageAuthPassword {user, password = HackagePassword secret})}
144+
Nothing Nothing (Just tokenSpec) -> do
145+
secret <- resolveSecret tokenSpec.secret
146+
pure location {auth = Just (HackageAuthToken {token = HackageToken secret})}
132147
where
133148
onlyOne present absent = clientError [exon|Specified a #{present}, but no #{absent}|]
134149

150+
bothSorts = clientError "Specified both password and auth token"
151+
135152
validateContextRepo :: ContextHackageRepo -> M HackageRepo
136153
validateContextRepo ContextHackageRepo {location = location0, ..} = do
137154
appContextVerbose [exon|validating the Hackage config #{Color.yellow name}|] do
138155
location1 <- for location0 \ (ContextHackageLocation spec) ->
139156
eitherClient (first toText (parseLocation (toString spec)))
140-
location <- withAuth (fromMaybe HackageLocation.central location1) user password
157+
location <- withAuth (fromMaybe HackageLocation.central location1) user password token
141158
pure HackageRepo {
142159
name,
143160
description = fromMaybe (hackageDescription location) description,

packages/hix/lib/Hix/Managed/Cabal/ContextHackageRepo.hs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import Hix.Managed.Cabal.Data.ContextHackageRepo (
1414
ContextHackageLocation (..),
1515
ContextHackagePassword (..),
1616
ContextHackageRepo (..),
17+
ContextHackageSecret (..),
18+
ContextHackageToken (..),
1719
contextHackageRepo,
1820
)
19-
import Hix.Managed.Cabal.Data.HackageLocation (HackagePassword (..), HackageUser (..))
21+
import Hix.Managed.Cabal.Data.HackageLocation (HackageSecret (..), HackageUser (..))
2022
import Hix.Managed.Cabal.Data.HackageRepo (HackageName, centralName)
2123

2224
update' ::
@@ -57,7 +59,8 @@ fields =
5759
("enable", update #enable bool),
5860
("location", update #location (text ContextHackageLocation)),
5961
("user", update #user (text HackageUser)),
60-
("password", update' True #password (text (PasswordPlain . HackagePassword))),
62+
("password", update' True #password (text (ContextHackagePassword . SecretPlain . HackageSecret))),
63+
("token", update' True #token (text (ContextHackageToken . SecretPlain . HackageSecret))),
6164
("secure", update #secure bool),
6265
("keys", update #keys (nonEmpty . Text.splitOn "," . toText)),
6366
("indexState", update #indexState simpleParsec),

packages/hix/lib/Hix/Managed/Cabal/Data/ContextHackageRepo.hs

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Text.PrettyPrint
77

88
import Hix.Class.EncodeNix (EncodeNix (encodeNix))
99
import Hix.Data.NixExpr (Expr (..), ExprAttr (..))
10-
import Hix.Managed.Cabal.Data.HackageLocation (HackagePassword (HackagePassword), HackageUser)
10+
import Hix.Managed.Cabal.Data.HackageLocation (HackageSecret (..), HackageUser)
1111
import Hix.Managed.Cabal.Data.HackageRepo (HackageDescription, HackageIndexState, HackageName)
1212
import Hix.NixExpr (mkAttrs, single, singleOpt)
1313
import Hix.Pretty (field, prettyFieldsV, prettyText)
@@ -20,52 +20,62 @@ newtype ContextHackageLocation =
2020
instance Pretty ContextHackageLocation where
2121
pretty = prettyText . coerce
2222

23-
data ContextHackagePassword =
23+
data ContextHackageSecret =
2424
-- | Password was intended to be printed, most likely in a test.
25-
PasswordUnobscured HackagePassword
25+
SecretUnobscured HackageSecret
2626
|
27-
PasswordPlain HackagePassword
27+
SecretPlain HackageSecret
2828
|
29-
PasswordEnvVar Text
29+
SecretEnvVar Text
3030
|
31-
PasswordExec Text
31+
SecretExec Text
3232
deriving stock (Eq, Show)
3333

34-
instance Pretty ContextHackagePassword where
34+
instance Pretty ContextHackageSecret where
3535
pretty = \case
36-
PasswordUnobscured (HackagePassword pw) -> prettyText pw
37-
PasswordPlain _ -> "<password>"
38-
PasswordEnvVar var -> prettyText var <+> brackets (text "env-var")
39-
PasswordExec exe -> prettyText exe <+> brackets (text "exec")
36+
SecretUnobscured (HackageSecret pw) -> prettyText pw
37+
SecretPlain _ -> "<password>"
38+
SecretEnvVar var -> prettyText var <+> brackets (text "env-var")
39+
SecretExec exe -> prettyText exe <+> brackets (text "exec")
4040

41-
instance EncodeNix ContextHackagePassword where
41+
instance EncodeNix ContextHackageSecret where
4242
encodeNix = \case
43-
PasswordUnobscured (HackagePassword pw) -> ExprString pw
44-
PasswordPlain _ -> ExprString "<password>"
45-
PasswordEnvVar var -> structured "env-var" var
46-
PasswordExec exe -> structured "exec" exe
43+
SecretUnobscured (HackageSecret pw) -> ExprString pw
44+
SecretPlain _ -> ExprString "<password>"
45+
SecretEnvVar var -> structured "env-var" var
46+
SecretExec exe -> structured "exec" exe
4747
where
4848
structured t value =
4949
ExprAttrs [
5050
ExprAttr "type" (ExprString t),
5151
ExprAttr {name = "value", value = ExprString value}
5252
]
5353

54-
instance FromJSON ContextHackagePassword where
54+
instance FromJSON ContextHackageSecret where
5555
parseJSON v =
56-
withText "ContextHackagePassword" plain v
56+
withText "ContextHackageSecret" plain v
5757
<|>
58-
withObject "ContextHackagePassword" typed v
58+
withObject "ContextHackageSecret" typed v
5959
where
6060
typed o = do
6161
value <- o .: "value"
6262
o .: "type" >>= \case
6363
("plain" :: Text) -> plain value
64-
"env-var" -> pure (PasswordEnvVar value)
65-
"exec" -> pure (PasswordExec value)
64+
"env-var" -> pure (SecretEnvVar value)
65+
"exec" -> pure (SecretExec value)
6666
t -> fail [exon|Invalid value for Hackage password type: ##{t}|]
6767

68-
plain = pure . PasswordPlain . HackagePassword
68+
plain = pure . SecretPlain . HackageSecret
69+
70+
newtype ContextHackagePassword =
71+
ContextHackagePassword { secret :: ContextHackageSecret }
72+
deriving stock (Eq, Show, Generic)
73+
deriving newtype (Pretty, EncodeNix, FromJSON)
74+
75+
newtype ContextHackageToken =
76+
ContextHackageToken { secret :: ContextHackageSecret }
77+
deriving stock (Eq, Show, Generic)
78+
deriving newtype (Pretty, EncodeNix, FromJSON)
6979

7080
data ContextHackageRepo =
7181
ContextHackageRepo {
@@ -75,6 +85,7 @@ data ContextHackageRepo =
7585
location :: Maybe ContextHackageLocation,
7686
user :: Maybe HackageUser,
7787
password :: Maybe ContextHackagePassword,
88+
token :: Maybe ContextHackageToken,
7889
secure :: Maybe Bool,
7990
keys :: Maybe (NonEmpty Text),
8091
indexState :: Maybe HackageIndexState,
@@ -93,6 +104,7 @@ instance Pretty ContextHackageRepo where
93104
field "location" location,
94105
field "user" user,
95106
field "password" password,
107+
field "token" token,
96108
field "secure" secure,
97109
field "keys" keys,
98110
field "indexState" indexState,
@@ -125,6 +137,7 @@ contextHackageRepo name =
125137
location = Nothing,
126138
user = Nothing,
127139
password = Nothing,
140+
token = Nothing,
128141
secure = Nothing,
129142
keys = Nothing,
130143
indexState = Nothing,

packages/hix/lib/Hix/Managed/Cabal/Data/HackageLocation.hs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,23 +60,39 @@ newtype HackageUser =
6060
instance Pretty HackageUser where
6161
pretty = prettyNt
6262

63-
newtype HackagePassword =
64-
HackagePassword Text
63+
newtype HackageSecret =
64+
HackageSecret { text :: Text }
6565
deriving stock (Eq)
6666
deriving newtype (IsString, Ord, FromJSON)
6767

68-
instance Show HackagePassword where
68+
instance Show HackageSecret where
6969
showsPrec d _ = showParen (d > 10) (showString "HackagePassword <password>")
7070

71-
instance Pretty HackagePassword where
71+
instance Pretty HackageSecret where
7272
pretty _ = "<password>"
7373

74+
newtype HackagePassword =
75+
HackagePassword { secret :: HackageSecret }
76+
deriving stock (Eq, Show)
77+
deriving newtype (IsString, Ord, FromJSON)
78+
79+
newtype HackageToken =
80+
HackageToken { secret :: HackageSecret }
81+
deriving stock (Eq, Show)
82+
deriving newtype (IsString, Ord, FromJSON)
83+
84+
data HackageAuth =
85+
HackageAuthPassword { user :: HackageUser, password :: HackagePassword }
86+
|
87+
HackageAuthToken { token :: HackageToken }
88+
deriving stock (Eq, Show)
89+
7490
data HackageLocation =
7591
HackageLocation {
7692
host :: HackageHost,
7793
tls :: HackageTls,
7894
port :: Maybe HackagePort,
79-
auth :: Maybe (HackageUser, HackagePassword)
95+
auth :: Maybe HackageAuth
8096
}
8197
deriving stock (Eq, Show, Generic)
8298

packages/hix/lib/Hix/Managed/Handlers/HackageClient/Prod.hs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Network.HTTP.Client.MultipartFormData (formDataBody, partBS)
1818
import Network.HTTP.Types (
1919
Status (statusCode, statusMessage),
2020
hAccept,
21+
hAuthorization,
2122
hContentType,
2223
statusIsClientError,
2324
statusIsServerError,
@@ -31,10 +32,13 @@ import Hix.Http (httpManager)
3132
import qualified Hix.Log as Log
3233
import Hix.Managed.Cabal.Data.Config (CabalConfig, HackagePurpose, hackagesFor)
3334
import Hix.Managed.Cabal.Data.HackageLocation (
35+
HackageAuth (..),
3436
HackageHost (..),
3537
HackageLocation (..),
36-
HackagePassword (HackagePassword),
38+
HackagePassword (..),
39+
HackageSecret (..),
3740
HackageTls (..),
41+
HackageToken (..),
3842
HackageUser (..),
3943
hackageTlsBool,
4044
)
@@ -101,8 +105,11 @@ nativeRequest location request@HackageRequest {..} = do
101105
Left fields -> formDataBody [partBS key (encodeUtf8 value) | (key, value) <- toList fields]
102106

103107
addAuth =
104-
maybe id \ (HackageUser user, HackagePassword password) ->
105-
applyBasicAuth (encodeUtf8 user) (encodeUtf8 password)
108+
maybe id \case
109+
HackageAuthPassword {user = HackageUser user, password = HackagePassword (HackageSecret password)} ->
110+
applyBasicAuth (encodeUtf8 user) (encodeUtf8 password)
111+
HackageAuthToken {token = HackageToken (HackageSecret token)} ->
112+
\ req -> req {requestHeaders = (hAuthorization, (encodeUtf8 token)) : req.requestHeaders}
106113

107114
addQuery = maybe id \ q -> setQueryString (second Just <$> toList q)
108115

@@ -178,8 +185,8 @@ handlersMock manager port = do
178185
host = "localhost",
179186
tls = TlsOff,
180187
port = Just (fromIntegral port),
181-
auth = Just ("admin", "admin")
188+
auth = Just (HackageAuthPassword {user = "admin", password = "admin"})
182189
}
183190
}
184-
userRes = res {location = res.location {auth = Just ("test", "test")}}
191+
userRes = res {location = res.location {auth = Just (HackageAuthPassword {user = "test", password = "test"})}}
185192
adminClient = handlersProd res

0 commit comments

Comments
 (0)