@@ -46,7 +46,7 @@ extension OPA {
4646 self . allowInsecureTLS = allowInsecureTLS
4747 self . responseHeaderTimeoutSeconds = responseHeaderTimeoutSeconds
4848 self . tls = tls
49- let credentials = credentials
49+ let credentials = credentials ?? . defaultNoAuth
5050 self . type = type
5151
5252 if case . bearer( let plugin) = credentials {
@@ -67,7 +67,7 @@ extension OPA {
6767 self . responseHeaderTimeoutSeconds = try container. decodeIfPresent (
6868 Int64 . self, forKey: . responseHeaderTimeoutSeconds)
6969 self . tls = try container. decodeIfPresent ( ServerTLSConfig . self, forKey: . tls)
70- let credentials = try container. decodeIfPresent ( Credentials . self, forKey: . credentials)
70+ let credentials = try container. decodeIfPresent ( Credentials . self, forKey: . credentials) ?? . defaultNoAuth
7171 self . type = try container. decodeIfPresent ( String . self, forKey: . type)
7272
7373 if case . bearer( let plugin) = credentials {
@@ -88,6 +88,7 @@ extension OPA {
8888 /// config keys in this section-- any configuration will appear under the
8989 /// `plugins` section.
9090 public enum Credentials : Codable , Sendable , Equatable {
91+ case defaultNoAuth
9192 case bearer( BearerAuthPlugin )
9293 case oauth2( [ String : AnyCodable ] )
9394 case clientTLS( ClientTLSAuthPlugin )
@@ -112,49 +113,62 @@ extension OPA {
112113 // Check if plugin field is present.
113114 if let pluginName = try container. decodeIfPresent ( String . self, forKey: . custom) {
114115 self = . custom( pluginName)
115- } else {
116- // Fall back to trying each credential type.
117- let attemptedCredentialTypes : [ Credentials ? ] = [
118- try ? container. decodeIfPresent ( BearerAuthPlugin . self, forKey: . bearer) . map { . bearer( $0) } ,
119- try ? container. decodeIfPresent ( [ String : AnyCodable ] . self, forKey: . oauth2) . map { . oauth2( $0) } ,
120- try ? container. decodeIfPresent ( ClientTLSAuthPlugin . self, forKey: . clientTLS) . map {
121- . clientTLS( $0)
122- } ,
123- try ? container. decodeIfPresent ( [ String : AnyCodable ] . self, forKey: . s3Signing) . map {
124- . s3Signing( $0)
125- } ,
126- try ? container. decodeIfPresent ( GCPMetadataAuthPlugin . self, forKey: . gcpMetadata) . map {
127- . gcpMetadata( $0)
128- } ,
129- try ? container. decodeIfPresent (
130- AzureManagedIdentitiesAuthPlugin . self, forKey: . azureManagedIdentity
131- )
132- . map {
133- . azureManagedIdentity( $0)
134- } ,
135- ]
136-
137- let foundCredentials = attemptedCredentialTypes. compactMap { $0 }
138-
139- guard foundCredentials. count == 1 else {
140- throw DecodingError . dataCorrupted (
141- DecodingError . Context (
142- codingPath: container. codingPath,
143- debugDescription: foundCredentials. isEmpty
144- ? " No valid credential type found "
145- : " Expected exactly one credential type, but found \( foundCredentials. count) "
146- )
116+ return
117+ }
118+
119+ // Determine which credential keys are actually present in the payload.
120+ let credentialKeys : [ CodingKeys ] = [
121+ . bearer, . oauth2, . clientTLS, . s3Signing, . gcpMetadata, . azureManagedIdentity,
122+ ]
123+ let presentKeys = credentialKeys. filter { container. contains ( $0) }
124+
125+ guard presentKeys. count <= 1 else {
126+ throw DecodingError . dataCorrupted (
127+ DecodingError . Context (
128+ codingPath: container. codingPath,
129+ debugDescription:
130+ " Expected at most one credential type, but found \( presentKeys. count) "
147131 )
148- }
132+ )
133+ }
149134
150- self = foundCredentials [ 0 ]
135+ // No credential keys present (e.g. empty `{}`): default to no auth.
136+ guard let key = presentKeys. first else {
137+ self = . defaultNoAuth
138+ return
139+ }
140+
141+ // Exactly one key present — decode it strictly so misconfigurations propagate.
142+ switch key {
143+ case . bearer:
144+ self = . bearer( try container. decode ( BearerAuthPlugin . self, forKey: . bearer) )
145+ case . oauth2:
146+ self = . oauth2( try container. decode ( [ String : AnyCodable ] . self, forKey: . oauth2) )
147+ case . clientTLS:
148+ self = . clientTLS( try container. decode ( ClientTLSAuthPlugin . self, forKey: . clientTLS) )
149+ case . s3Signing:
150+ self = . s3Signing( try container. decode ( [ String : AnyCodable ] . self, forKey: . s3Signing) )
151+ case . gcpMetadata:
152+ self = . gcpMetadata( try container. decode ( GCPMetadataAuthPlugin . self, forKey: . gcpMetadata) )
153+ case . azureManagedIdentity:
154+ self = . azureManagedIdentity(
155+ try container. decode ( AzureManagedIdentitiesAuthPlugin . self, forKey: . azureManagedIdentity) )
156+ default :
157+ throw DecodingError . dataCorrupted (
158+ DecodingError . Context (
159+ codingPath: container. codingPath,
160+ debugDescription: " Unexpected credential key: \( key. stringValue) "
161+ )
162+ )
151163 }
152164 }
153165
154166 public func encode( to encoder: Encoder ) throws {
155167 var container = encoder. container ( keyedBy: CodingKeys . self)
156168
157169 switch self {
170+ case . defaultNoAuth:
171+ break
158172 case . bearer( let plugin) :
159173 try container. encode ( plugin, forKey: . bearer)
160174 case . oauth2( let config) :
@@ -175,6 +189,16 @@ extension OPA {
175189
176190 // Validates struct-local constraints.
177191 public func validate( ) throws {
192+ // Ensure URL is a valid HTTP/HTTPS URL.
193+ guard let scheme = self . url. scheme? . lowercased ( ) ,
194+ scheme == " http " || scheme == " https " ,
195+ self . url. host != nil
196+ else {
197+ throw OPA . ConfigError (
198+ code: . internalError, message: " Expected a valid http/https URL, got: \( self . url) " )
199+ }
200+
201+ // For credentials types that need extra context, we validate those here.
178202 switch self . credentials {
179203 case . clientTLS( let plugin) :
180204 try plugin. validateWithContext ( serviceTLS: self . tls)
0 commit comments