@@ -2,74 +2,92 @@ package is.hail.services
22
33import is .hail .services .oauth2 .AzureCloudCredentials .AzureTokenRefreshMinutes
44import is .hail .services .oauth2 .AzureCloudCredentials .EnvVars .AzureApplicationCredentials
5- import is .hail .services .oauth2 .GoogleCloudCredentials .EnvVars .GoogleApplicationCredentials
65import is .hail .shadedazure .com .azure .core .credential .{
76 AccessToken , TokenCredential , TokenRequestContext ,
87}
98import is .hail .shadedazure .com .azure .identity .{
109 ClientSecretCredentialBuilder , DefaultAzureCredentialBuilder ,
1110}
12- import is .hail .utils .{defaultJSONFormats , using }
11+ import is .hail .utils .{jsonToBytes , using }
1312
1413import scala .collection .JavaConverters ._
1514
16- import java .io .Serializable
15+ import java .io .{ ByteArrayInputStream , Serializable }
1716import java .nio .file .{Files , Path }
1817import java .time .OffsetDateTime
1918
20- import com .google .auth .oauth2 .{ GoogleCredentials , ServiceAccountCredentials }
21- import org .json4s .Formats
19+ import com .google .auth .oauth2 .GoogleCredentials
20+ import org .json4s .{ DefaultFormats , Formats , JValue }
2221import org .json4s .jackson .JsonMethods
2322
2423object oauth2 {
2524
2625 sealed trait CloudCredentials extends Product with Serializable {
2726 def accessToken : String
27+ def scoped (scopes : Array [String ]): CloudCredentials
2828 }
2929
30- def CloudCredentials (
31- keyPath : Path ,
32- scopes : IndexedSeq [String ],
33- env : Map [String , String ] = sys.env,
34- ): CloudCredentials =
30+ implicit lazy val fmts : Formats = DefaultFormats
31+
32+ def HailCredentials (env : Map [String , String ] = sys.env): Option [CloudCredentials ] =
33+ for {
34+ config <-
35+ env
36+ .get(" XDG_CONFIG_HOME" )
37+ .map(Path .of(_))
38+ .orElse(env.get(" HOME" ).map(Path .of(_, " .config" )))
39+
40+ identity = config.resolve(" hail/identity.json" ).toFile
41+ if identity.exists()
42+
43+ jvalue <- JsonMethods .parseOpt(identity)
44+ } yield (jvalue \ " idp" ).extract[String ] match {
45+ case " Google" => GoogleCloudCredentials .fromJson(jvalue \ " credentials" )
46+ case " Microsoft" => AzureCloudCredentials .fromJson(jvalue \ " credentials" )
47+ case other => throw new IllegalArgumentException (s " Unknown identity provider: ' $other' " )
48+ }
49+
50+ def CloudCredentials (keyPath : Option [Path ], env : Map [String , String ] = sys.env)
51+ : CloudCredentials =
3552 env.get(" HAIL_CLOUD" ) match {
36- case Some (" gcp" ) => GoogleCloudCredentials (Some ( keyPath), scopes, env )
37- case Some (" azure" ) => AzureCloudCredentials (Some ( keyPath), scopes , env)
53+ case None | Some (" gcp" ) => GoogleCloudCredentials (keyPath)
54+ case Some (" azure" ) => AzureCloudCredentials (keyPath, env)
3855 case Some (cloud) => throw new IllegalArgumentException (s " Unknown cloud: ' $cloud' " )
39- case None => throw new IllegalArgumentException (s " HAIL_CLOUD must be set. " )
4056 }
4157
4258 case class GoogleCloudCredentials (value : GoogleCredentials ) extends CloudCredentials {
4359 override def accessToken : String = {
4460 value.refreshIfExpired()
4561 value.getAccessToken.getTokenValue
4662 }
63+
64+ override def scoped (scopes : Array [String ]): GoogleCloudCredentials =
65+ GoogleCloudCredentials (value.createScoped(scopes : _* ))
4766 }
4867
4968 object GoogleCloudCredentials {
50- object EnvVars {
51- val GoogleApplicationCredentials = " GOOGLE_APPLICATION_CREDENTIALS"
52- }
53-
54- def apply (keyPath : Option [Path ], scopes : IndexedSeq [String ], env : Map [String , String ] = sys.env)
55- : GoogleCloudCredentials =
69+ def fromJson (jv : JValue ): GoogleCloudCredentials =
5670 GoogleCloudCredentials {
57- val creds : GoogleCredentials =
58- keyPath.orElse(env.get(GoogleApplicationCredentials ).map(Path .of(_))) match {
59- case Some (path) =>
60- using(Files .newInputStream(path))(ServiceAccountCredentials .fromStream)
61- case None =>
62- GoogleCredentials .getApplicationDefault
63- }
71+ GoogleCredentials .fromStream(
72+ new ByteArrayInputStream (jsonToBytes(jv))
73+ )
74+ }
6475
65- creds.createScoped(scopes : _* )
76+ def apply (keyPath : Option [Path ]): GoogleCloudCredentials =
77+ GoogleCloudCredentials {
78+ keyPath match {
79+ case Some (path) =>
80+ using(Files .newInputStream(path))(GoogleCredentials .fromStream)
81+ case None =>
82+ GoogleCredentials .getApplicationDefault
83+ }
6684 }
6785 }
6886
6987 sealed trait AzureCloudCredentials extends CloudCredentials {
7088
7189 def value : TokenCredential
72- def scopes : IndexedSeq [String ]
90+ def scopes : Array [String ]
7391
7492 @ transient private [this ] var token : AccessToken = _
7593
@@ -78,11 +96,13 @@ object oauth2 {
7896 token.getToken
7997 }
8098
99+ override def scoped (scopes : Array [String ]): AzureCloudCredentials
100+
81101 private [this ] def refreshIfRequired (): Unit =
82102 if (! isExpired) token.getToken
83103 else synchronized {
84104 if (isExpired) {
85- token = value.getTokenSync(new TokenRequestContext ().setScopes(scopes.asJava))
105+ token = value.getTokenSync(new TokenRequestContext ().setScopes(scopes.toSeq. asJava))
86106 }
87107
88108 token.getToken: Unit
@@ -99,33 +119,52 @@ object oauth2 {
99119 val AzureApplicationCredentials = " AZURE_APPLICATION_CREDENTIALS"
100120 }
101121
122+ val DefaultOAuth2Scopes : Array [String ] =
123+ Array (" .default" )
124+
102125 private [AzureCloudCredentials ] val AzureTokenRefreshMinutes = 5
103126
104- def apply (keyPath : Option [Path ], scopes : IndexedSeq [String ], env : Map [String , String ] = sys.env)
105- : AzureCloudCredentials =
127+ def fromJson (jv : JValue , scopes : Array [String ] = DefaultOAuth2Scopes ): AzureCloudCredentials =
128+ AzureClientSecretCredentials (
129+ clientId = (jv \ " appId" ).extract[String ],
130+ tenantId = (jv \ " tenant" ).extract[String ],
131+ secret = (jv \ " password" ).extract[String ],
132+ scopes = scopes,
133+ )
134+
135+ def apply (keyPath : Option [Path ], env : Map [String , String ] = sys.env): AzureCloudCredentials =
106136 keyPath.orElse(env.get(AzureApplicationCredentials ).map(Path .of(_))) match {
107- case Some (path) => AzureClientSecretCredentials (path, scopes)
108- case None => AzureDefaultCredentials (scopes)
137+ case Some (path) =>
138+ using(Files .newInputStream(path)) { in =>
139+ fromJson(JsonMethods .parse(in), DefaultOAuth2Scopes )
140+ }
141+ case None =>
142+ AzureDefaultCredentials (DefaultOAuth2Scopes )
109143 }
110144 }
111145
112- private case class AzureDefaultCredentials (scopes : IndexedSeq [String ])
113- extends AzureCloudCredentials {
146+ private case class AzureDefaultCredentials (scopes : Array [String ]) extends AzureCloudCredentials {
114147 @ transient override lazy val value : TokenCredential =
115148 new DefaultAzureCredentialBuilder ().build()
149+
150+ override def scoped (scopes : Array [String ]): AzureDefaultCredentials =
151+ copy(scopes)
116152 }
117153
118- private case class AzureClientSecretCredentials (path : Path , scopes : IndexedSeq [String ])
119- extends AzureCloudCredentials {
154+ private case class AzureClientSecretCredentials (
155+ clientId : String ,
156+ tenantId : String ,
157+ secret : String ,
158+ scopes : Array [String ],
159+ ) extends AzureCloudCredentials {
120160 @ transient override lazy val value : TokenCredential =
121- using(Files .newInputStream(path)) { is =>
122- implicit val fmts : Formats = defaultJSONFormats
123- val kvs = JsonMethods .parse(is)
124- new ClientSecretCredentialBuilder ()
125- .clientId((kvs \ " appId" ).extract[String ])
126- .clientSecret((kvs \ " password" ).extract[String ])
127- .tenantId((kvs \ " tenant" ).extract[String ])
128- .build()
129- }
161+ new ClientSecretCredentialBuilder ()
162+ .clientId(clientId)
163+ .clientSecret(secret)
164+ .tenantId(tenantId)
165+ .build()
166+
167+ override def scoped (scopes : Array [String ]): AzureClientSecretCredentials =
168+ copy(scopes = scopes)
130169 }
131170}
0 commit comments