diff --git a/clients/go/zms/model.go b/clients/go/zms/model.go index 5e11527b13b..c11cedd039f 100644 --- a/clients/go/zms/model.go +++ b/clients/go/zms/model.go @@ -302,6 +302,16 @@ type DomainMeta struct { // AzureSubscription string `json:"azureSubscription" rdl:"optional" yaml:",omitempty"` + // + // associated azure tenant id (system attribute) + // + AzureTenant string `json:"azureTenant" rdl:"optional" yaml:",omitempty"` + + // + // associated azure client id (system attribute) + // + AzureClient string `json:"azureClient" rdl:"optional" yaml:",omitempty"` + // // associated gcp project id (system attribute - uniqueness check - if // enabled) @@ -443,6 +453,18 @@ func (self *DomainMeta) Validate() error { return fmt.Errorf("DomainMeta.azureSubscription does not contain a valid String (%v)", val.Error) } } + if self.AzureTenant != "" { + val := rdl.Validate(ZMSSchema(), "String", self.AzureTenant) + if !val.Valid { + return fmt.Errorf("DomainMeta.azureTenant does not contain a valid String (%v)", val.Error) + } + } + if self.AzureClient != "" { + val := rdl.Validate(ZMSSchema(), "String", self.AzureClient) + if !val.Valid { + return fmt.Errorf("DomainMeta.azureClient does not contain a valid String (%v)", val.Error) + } + } if self.GcpProject != "" { val := rdl.Validate(ZMSSchema(), "String", self.GcpProject) if !val.Valid { @@ -574,6 +596,16 @@ type Domain struct { // AzureSubscription string `json:"azureSubscription" rdl:"optional" yaml:",omitempty"` + // + // associated azure tenant id (system attribute) + // + AzureTenant string `json:"azureTenant" rdl:"optional" yaml:",omitempty"` + + // + // associated azure client id (system attribute) + // + AzureClient string `json:"azureClient" rdl:"optional" yaml:",omitempty"` + // // associated gcp project id (system attribute - uniqueness check - if // enabled) @@ -730,6 +762,18 @@ func (self *Domain) Validate() error { return fmt.Errorf("Domain.azureSubscription does not contain a valid String (%v)", val.Error) } } + if self.AzureTenant != "" { + val := rdl.Validate(ZMSSchema(), "String", self.AzureTenant) + if !val.Valid { + return fmt.Errorf("DomainMeta.azureTenant does not contain a valid String (%v)", val.Error) + } + } + if self.AzureClient != "" { + val := rdl.Validate(ZMSSchema(), "String", self.AzureClient) + if !val.Valid { + return fmt.Errorf("DomainMeta.azureClient does not contain a valid String (%v)", val.Error) + } + } if self.GcpProject != "" { val := rdl.Validate(ZMSSchema(), "String", self.GcpProject) if !val.Valid { @@ -3997,6 +4041,16 @@ type TopLevelDomain struct { // AzureSubscription string `json:"azureSubscription" rdl:"optional" yaml:",omitempty"` + // + // associated azure tenant id (system attribute) + // + AzureTenant string `json:"azureTenant" rdl:"optional" yaml:",omitempty"` + + // + // associated azure client id (system attribute) + // + AzureClient string `json:"azureClient" rdl:"optional" yaml:",omitempty"` + // // associated gcp project id (system attribute - uniqueness check - if // enabled) @@ -4156,6 +4210,18 @@ func (self *TopLevelDomain) Validate() error { return fmt.Errorf("TopLevelDomain.azureSubscription does not contain a valid String (%v)", val.Error) } } + if self.AzureTenant != "" { + val := rdl.Validate(ZMSSchema(), "String", self.AzureTenant) + if !val.Valid { + return fmt.Errorf("DomainMeta.azureTenant does not contain a valid String (%v)", val.Error) + } + } + if self.AzureClient != "" { + val := rdl.Validate(ZMSSchema(), "String", self.AzureClient) + if !val.Valid { + return fmt.Errorf("DomainMeta.azureClient does not contain a valid String (%v)", val.Error) + } + } if self.GcpProject != "" { val := rdl.Validate(ZMSSchema(), "String", self.GcpProject) if !val.Valid { @@ -4293,6 +4359,16 @@ type SubDomain struct { // AzureSubscription string `json:"azureSubscription" rdl:"optional" yaml:",omitempty"` + // + // associated azure tenant id (system attribute) + // + AzureTenant string `json:"azureTenant" rdl:"optional" yaml:",omitempty"` + + // + // associated azure client id (system attribute) + // + AzureClient string `json:"azureClient" rdl:"optional" yaml:",omitempty"` + // // associated gcp project id (system attribute - uniqueness check - if // enabled) @@ -4457,6 +4533,18 @@ func (self *SubDomain) Validate() error { return fmt.Errorf("SubDomain.azureSubscription does not contain a valid String (%v)", val.Error) } } + if self.AzureTenant != "" { + val := rdl.Validate(ZMSSchema(), "String", self.AzureTenant) + if !val.Valid { + return fmt.Errorf("DomainMeta.azureTenant does not contain a valid String (%v)", val.Error) + } + } + if self.AzureClient != "" { + val := rdl.Validate(ZMSSchema(), "String", self.AzureClient) + if !val.Valid { + return fmt.Errorf("DomainMeta.azureClient does not contain a valid String (%v)", val.Error) + } + } if self.GcpProject != "" { val := rdl.Validate(ZMSSchema(), "String", self.GcpProject) if !val.Valid { @@ -4603,6 +4691,16 @@ type UserDomain struct { // AzureSubscription string `json:"azureSubscription" rdl:"optional" yaml:",omitempty"` + // + // associated azure tenant id (system attribute) + // + AzureTenant string `json:"azureTenant" rdl:"optional" yaml:",omitempty"` + + // + // associated azure client id (system attribute) + // + AzureClient string `json:"azureClient" rdl:"optional" yaml:",omitempty"` + // // associated gcp project id (system attribute - uniqueness check - if // enabled) @@ -4754,6 +4852,18 @@ func (self *UserDomain) Validate() error { return fmt.Errorf("UserDomain.azureSubscription does not contain a valid String (%v)", val.Error) } } + if self.AzureTenant != "" { + val := rdl.Validate(ZMSSchema(), "String", self.AzureTenant) + if !val.Valid { + return fmt.Errorf("DomainMeta.azureTenant does not contain a valid String (%v)", val.Error) + } + } + if self.AzureClient != "" { + val := rdl.Validate(ZMSSchema(), "String", self.AzureClient) + if !val.Valid { + return fmt.Errorf("DomainMeta.azureClient does not contain a valid String (%v)", val.Error) + } + } if self.GcpProject != "" { val := rdl.Validate(ZMSSchema(), "String", self.GcpProject) if !val.Valid { @@ -7228,6 +7338,16 @@ type DomainData struct { // AzureSubscription string `json:"azureSubscription" rdl:"optional" yaml:",omitempty"` + // + // associated azure tenant id (system attribute) + // + AzureTenant string `json:"azureTenant" rdl:"optional" yaml:",omitempty"` + + // + // associated azure client id (system attribute) + // + AzureClient string `json:"azureClient" rdl:"optional" yaml:",omitempty"` + // // associated gcp project id (system attribute - uniqueness check - if // enabled) @@ -7419,6 +7539,18 @@ func (self *DomainData) Validate() error { return fmt.Errorf("DomainData.azureSubscription does not contain a valid String (%v)", val.Error) } } + if self.AzureTenant != "" { + val := rdl.Validate(ZMSSchema(), "String", self.AzureTenant) + if !val.Valid { + return fmt.Errorf("DomainMeta.azureTenant does not contain a valid String (%v)", val.Error) + } + } + if self.AzureClient != "" { + val := rdl.Validate(ZMSSchema(), "String", self.AzureClient) + if !val.Valid { + return fmt.Errorf("DomainMeta.azureClient does not contain a valid String (%v)", val.Error) + } + } if self.GcpProject != "" { val := rdl.Validate(ZMSSchema(), "String", self.GcpProject) if !val.Valid { diff --git a/clients/go/zms/zms_schema.go b/clients/go/zms/zms_schema.go index d1f77e928c7..4c7fa81070c 100644 --- a/clients/go/zms/zms_schema.go +++ b/clients/go/zms/zms_schema.go @@ -164,6 +164,8 @@ func init() { tDomainMeta.Field("groupExpiryDays", "Int32", true, nil, "all groups in the domain roles will have specified max expiry days") tDomainMeta.Field("userAuthorityFilter", "String", true, nil, "membership filtered based on user authority configured attributes") tDomainMeta.Field("azureSubscription", "String", true, nil, "associated azure subscription id (system attribute - uniqueness check - if enabled)") + tDomainMeta.Field("azureTenant", "String", true, nil, "associated azure tenant id (system attribute)") + tDomainMeta.Field("azureClient", "String", true, nil, "associated azure client id (system attribute)") tDomainMeta.Field("gcpProject", "String", true, nil, "associated gcp project id (system attribute - uniqueness check - if enabled)") tDomainMeta.Field("gcpProjectNumber", "String", true, nil, "associated gcp project number (system attribute)") tDomainMeta.MapField("tags", "TagKey", "TagValueList", true, "key-value pair tags, tag might contain multiple values") diff --git a/clients/go/zts/model.go b/clients/go/zts/model.go index 0a50341d962..6533611db3f 100644 --- a/clients/go/zts/model.go +++ b/clients/go/zts/model.go @@ -4212,6 +4212,16 @@ type DomainDetails struct { // AzureSubscription string `json:"azureSubscription,omitempty" rdl:"optional"` + // + // associated azure tenant id + // + AzureTenant string `json:"azureTenant,omitempty" rdl:"optional"` + + // + // associated azure client id + // + AzureClient string `json:"azureClient,omitempty" rdl:"optional"` + // // associated gcp project id // @@ -4270,6 +4280,18 @@ func (self *DomainDetails) Validate() error { return fmt.Errorf("DomainDetails.azureSubscription does not contain a valid String (%v)", val.Error) } } + if self.AzureTenant != "" { + val := rdl.Validate(ZTSSchema(), "String", self.AzureTenant) + if !val.Valid { + return fmt.Errorf("DomainDetails.azureTenant does not contain a valid String (%v)", val.Error) + } + } + if self.AzureClient != "" { + val := rdl.Validate(ZTSSchema(), "String", self.AzureClient) + if !val.Valid { + return fmt.Errorf("DomainDetails.azureClient does not contain a valid String (%v)", val.Error) + } + } if self.GcpProjectId != "" { val := rdl.Validate(ZTSSchema(), "String", self.GcpProjectId) if !val.Valid { diff --git a/clients/go/zts/zts_schema.go b/clients/go/zts/zts_schema.go index 243546454e6..4db575895c3 100644 --- a/clients/go/zts/zts_schema.go +++ b/clients/go/zts/zts_schema.go @@ -523,6 +523,8 @@ func init() { tDomainDetails.Field("name", "DomainName", false, nil, "name of the athenz domain") tDomainDetails.Field("awsAccount", "String", true, nil, "associated aws account id") tDomainDetails.Field("azureSubscription", "String", true, nil, "associated azure subscription id") + tDomainDetails.Field("azureTenant", "String", true, nil, "associated azure tenant id") + tDomainDetails.Field("azureClient", "String", true, nil, "associated azure client id") tDomainDetails.Field("gcpProjectId", "String", true, nil, "associated gcp project id") tDomainDetails.Field("gcpProjectNumber", "String", true, nil, "associated gcp project number") sb.AddType(tDomainDetails.Build()) diff --git a/core/zms/src/main/java/com/yahoo/athenz/zms/Domain.java b/core/zms/src/main/java/com/yahoo/athenz/zms/Domain.java index 87b7a15e7dd..0f9abfc40a2 100644 --- a/core/zms/src/main/java/com/yahoo/athenz/zms/Domain.java +++ b/core/zms/src/main/java/com/yahoo/athenz/zms/Domain.java @@ -71,6 +71,12 @@ public class Domain { public String azureSubscription; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) + public String azureTenant; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public String azureClient; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_EMPTY) public String gcpProject; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -226,6 +232,20 @@ public Domain setAzureSubscription(String azureSubscription) { public String getAzureSubscription() { return azureSubscription; } + public Domain setAzureTenant(String azureTenant) { + this.azureTenant = azureTenant; + return this; + } + public String getAzureTenant() { + return azureTenant; + } + public Domain setAzureClient(String azureClient) { + this.azureClient = azureClient; + return this; + } + public String getAzureClient() { + return azureClient; + } public Domain setGcpProject(String gcpProject) { this.gcpProject = gcpProject; return this; @@ -376,6 +396,12 @@ public boolean equals(Object another) { if (azureSubscription == null ? a.azureSubscription != null : !azureSubscription.equals(a.azureSubscription)) { return false; } + if (azureTenant == null ? a.azureTenant != null : !azureTenant.equals(a.azureTenant)) { + return false; + } + if (azureClient == null ? a.azureClient != null : !azureClient.equals(a.azureClient)) { + return false; + } if (gcpProject == null ? a.gcpProject != null : !gcpProject.equals(a.gcpProject)) { return false; } diff --git a/core/zms/src/main/java/com/yahoo/athenz/zms/DomainData.java b/core/zms/src/main/java/com/yahoo/athenz/zms/DomainData.java index f900643b67e..7f8aca4388e 100644 --- a/core/zms/src/main/java/com/yahoo/athenz/zms/DomainData.java +++ b/core/zms/src/main/java/com/yahoo/athenz/zms/DomainData.java @@ -67,6 +67,12 @@ public class DomainData { public String azureSubscription; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) + public String azureTenant; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public String azureClient; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_EMPTY) public String gcpProject; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -222,6 +228,20 @@ public DomainData setAzureSubscription(String azureSubscription) { public String getAzureSubscription() { return azureSubscription; } + public DomainData setAzureTenant(String azureTenant) { + this.azureTenant = azureTenant; + return this; + } + public String getAzureTenant() { + return azureTenant; + } + public DomainData setAzureClient(String azureClient) { + this.azureClient = azureClient; + return this; + } + public String getAzureClient() { + return azureClient; + } public DomainData setGcpProject(String gcpProject) { this.gcpProject = gcpProject; return this; @@ -400,6 +420,12 @@ public boolean equals(Object another) { if (azureSubscription == null ? a.azureSubscription != null : !azureSubscription.equals(a.azureSubscription)) { return false; } + if (azureTenant == null ? a.azureTenant != null : !azureTenant.equals(a.azureTenant)) { + return false; + } + if (azureClient == null ? a.azureClient != null : !azureClient.equals(a.azureClient)) { + return false; + } if (gcpProject == null ? a.gcpProject != null : !gcpProject.equals(a.gcpProject)) { return false; } diff --git a/core/zms/src/main/java/com/yahoo/athenz/zms/DomainMeta.java b/core/zms/src/main/java/com/yahoo/athenz/zms/DomainMeta.java index efaf844bd74..af6e69fc531 100644 --- a/core/zms/src/main/java/com/yahoo/athenz/zms/DomainMeta.java +++ b/core/zms/src/main/java/com/yahoo/athenz/zms/DomainMeta.java @@ -67,6 +67,12 @@ public class DomainMeta { public String azureSubscription; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) + public String azureTenant; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public String azureClient; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_EMPTY) public String gcpProject; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -215,6 +221,20 @@ public DomainMeta setAzureSubscription(String azureSubscription) { public String getAzureSubscription() { return azureSubscription; } + public DomainMeta setAzureTenant(String azureTenant) { + this.azureTenant = azureTenant; + return this; + } + public String getAzureTenant() { + return azureTenant; + } + public DomainMeta setAzureClient(String azureClient) { + this.azureClient = azureClient; + return this; + } + public String getAzureClient() { + return azureClient; + } public DomainMeta setGcpProject(String gcpProject) { this.gcpProject = gcpProject; return this; @@ -344,6 +364,12 @@ public boolean equals(Object another) { if (azureSubscription == null ? a.azureSubscription != null : !azureSubscription.equals(a.azureSubscription)) { return false; } + if (azureTenant == null ? a.azureTenant != null : !azureTenant.equals(a.azureTenant)) { + return false; + } + if (azureClient == null ? a.azureClient != null : !azureClient.equals(a.azureClient)) { + return false; + } if (gcpProject == null ? a.gcpProject != null : !gcpProject.equals(a.gcpProject)) { return false; } diff --git a/core/zms/src/main/java/com/yahoo/athenz/zms/SubDomain.java b/core/zms/src/main/java/com/yahoo/athenz/zms/SubDomain.java index 46247a08083..02ee5e91cea 100644 --- a/core/zms/src/main/java/com/yahoo/athenz/zms/SubDomain.java +++ b/core/zms/src/main/java/com/yahoo/athenz/zms/SubDomain.java @@ -67,6 +67,11 @@ public class SubDomain { public String azureSubscription; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) + public String azureTenant; + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public String azureClient; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_EMPTY) public String gcpProject; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -221,6 +226,20 @@ public SubDomain setAzureSubscription(String azureSubscription) { public String getAzureSubscription() { return azureSubscription; } + public SubDomain setAzureTenant(String azureTenant) { + this.azureTenant = azureTenant; + return this; + } + public String getAzureTenant() { + return azureTenant; + } + public SubDomain setAzureClient(String azureClient) { + this.azureClient = azureClient; + return this; + } + public String getAzureClient() { + return azureClient; + } public SubDomain setGcpProject(String gcpProject) { this.gcpProject = gcpProject; return this; @@ -378,6 +397,12 @@ public boolean equals(Object another) { if (azureSubscription == null ? a.azureSubscription != null : !azureSubscription.equals(a.azureSubscription)) { return false; } + if (azureTenant == null ? a.azureTenant != null : !azureTenant.equals(a.azureTenant)) { + return false; + } + if (azureClient == null ? a.azureClient != null : !azureClient.equals(a.azureClient)) { + return false; + } if (gcpProject == null ? a.gcpProject != null : !gcpProject.equals(a.gcpProject)) { return false; } diff --git a/core/zms/src/main/java/com/yahoo/athenz/zms/TopLevelDomain.java b/core/zms/src/main/java/com/yahoo/athenz/zms/TopLevelDomain.java index 6dc61501873..71d8284c5e3 100644 --- a/core/zms/src/main/java/com/yahoo/athenz/zms/TopLevelDomain.java +++ b/core/zms/src/main/java/com/yahoo/athenz/zms/TopLevelDomain.java @@ -68,6 +68,12 @@ public class TopLevelDomain { public String azureSubscription; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) + public String azureTenant; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public String azureClient; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_EMPTY) public String gcpProject; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -221,6 +227,20 @@ public TopLevelDomain setAzureSubscription(String azureSubscription) { public String getAzureSubscription() { return azureSubscription; } + public TopLevelDomain setAzureTenant(String azureTenant) { + this.azureTenant = azureTenant; + return this; + } + public String getAzureTenant() { + return azureTenant; + } + public TopLevelDomain setAzureClient(String azureClient) { + this.azureClient = azureClient; + return this; + } + public String getAzureClient() { + return azureClient; + } public TopLevelDomain setGcpProject(String gcpProject) { this.gcpProject = gcpProject; return this; @@ -371,6 +391,12 @@ public boolean equals(Object another) { if (azureSubscription == null ? a.azureSubscription != null : !azureSubscription.equals(a.azureSubscription)) { return false; } + if (azureTenant == null ? a.azureTenant != null : !azureTenant.equals(a.azureTenant)) { + return false; + } + if (azureClient == null ? a.azureClient != null : !azureClient.equals(a.azureClient)) { + return false; + } if (gcpProject == null ? a.gcpProject != null : !gcpProject.equals(a.gcpProject)) { return false; } diff --git a/core/zms/src/main/java/com/yahoo/athenz/zms/UserDomain.java b/core/zms/src/main/java/com/yahoo/athenz/zms/UserDomain.java index cd68932d3db..3f1190dc8b8 100644 --- a/core/zms/src/main/java/com/yahoo/athenz/zms/UserDomain.java +++ b/core/zms/src/main/java/com/yahoo/athenz/zms/UserDomain.java @@ -67,6 +67,12 @@ public class UserDomain { public String azureSubscription; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) + public String azureTenant; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public String azureClient; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_EMPTY) public String gcpProject; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -219,6 +225,20 @@ public UserDomain setAzureSubscription(String azureSubscription) { public String getAzureSubscription() { return azureSubscription; } + public UserDomain setAzureTenant(String azureTenant) { + this.azureTenant = azureTenant; + return this; + } + public String getAzureTenant() { + return azureTenant; + } + public UserDomain setAzureClient(String azureClient) { + this.azureClient = azureClient; + return this; + } + public String getAzureClient() { + return azureClient; + } public UserDomain setGcpProject(String gcpProject) { this.gcpProject = gcpProject; return this; @@ -362,6 +382,12 @@ public boolean equals(Object another) { if (azureSubscription == null ? a.azureSubscription != null : !azureSubscription.equals(a.azureSubscription)) { return false; } + if (azureTenant == null ? a.azureTenant != null : !azureTenant.equals(a.azureTenant)) { + return false; + } + if (azureClient == null ? a.azureClient != null : !azureClient.equals(a.azureClient)) { + return false; + } if (gcpProject == null ? a.gcpProject != null : !gcpProject.equals(a.gcpProject)) { return false; } diff --git a/core/zms/src/main/java/com/yahoo/athenz/zms/ZMSSchema.java b/core/zms/src/main/java/com/yahoo/athenz/zms/ZMSSchema.java index a9300d9f520..792988e0aeb 100644 --- a/core/zms/src/main/java/com/yahoo/athenz/zms/ZMSSchema.java +++ b/core/zms/src/main/java/com/yahoo/athenz/zms/ZMSSchema.java @@ -139,6 +139,8 @@ private static Schema build() { .field("groupExpiryDays", "Int32", true, "all groups in the domain roles will have specified max expiry days") .field("userAuthorityFilter", "String", true, "membership filtered based on user authority configured attributes") .field("azureSubscription", "String", true, "associated azure subscription id (system attribute - uniqueness check - if enabled)") + .field("azureTenant", "String", true, "associated azure tenant id (system attribute)") + .field("azureClient", "String", true, "associated azure client id (system attribute)") .field("gcpProject", "String", true, "associated gcp project id (system attribute - uniqueness check - if enabled)") .field("gcpProjectNumber", "String", true, "associated gcp project number (system attribute)") .mapField("tags", "TagKey", "TagValueList", true, "key-value pair tags, tag might contain multiple values") diff --git a/core/zms/src/main/rdl/Domain.tdl b/core/zms/src/main/rdl/Domain.tdl index faca3d93bdd..32170d6772d 100644 --- a/core/zms/src/main/rdl/Domain.tdl +++ b/core/zms/src/main/rdl/Domain.tdl @@ -29,6 +29,8 @@ type DomainMeta Struct { Int32 groupExpiryDays (optional); //all groups in the domain roles will have specified max expiry days String userAuthorityFilter (optional); //membership filtered based on user authority configured attributes String azureSubscription (optional); //associated azure subscription id (system attribute - uniqueness check - if enabled) + String azureTenant (optional); //associated azure tenant id (system attribute) + String azureClient (optional); //associated azure client id (system attribute) String gcpProject (optional); //associated gcp project id (system attribute - uniqueness check - if enabled) String gcpProjectNumber (optional); //associated gcp project number (system attribute) Map tags (optional); //key-value pair tags, tag might contain multiple values diff --git a/core/zms/src/test/java/com/yahoo/athenz/zms/DomainTest.java b/core/zms/src/test/java/com/yahoo/athenz/zms/DomainTest.java index 81bf1587518..f7141a2e190 100644 --- a/core/zms/src/test/java/com/yahoo/athenz/zms/DomainTest.java +++ b/core/zms/src/test/java/com/yahoo/athenz/zms/DomainTest.java @@ -82,7 +82,8 @@ public void testDomainMetaMethod() { .setCertDnsDomain("athenz.cloud").setMemberExpiryDays(30).setTokenExpiryMins(300) .setServiceCertExpiryMins(120).setRoleCertExpiryMins(150).setSignAlgorithm("ec") .setServiceExpiryDays(40).setUserAuthorityFilter("OnShore").setGroupExpiryDays(50) - .setAzureSubscription("azure").setGcpProject("gcp").setBusinessService("business-service") + .setAzureSubscription("azure").setAzureTenant("tenant").setAzureClient("client") + .setGcpProject("gcp").setBusinessService("business-service") .setTags(Collections.singletonMap("tagKey", new TagValueList().setList(Collections.singletonList("tagValue")))) .setMemberPurgeExpiryDays(10).setGcpProjectNumber("1240").setProductId("abcd-1234") .setFeatureFlags(3).setContacts(Map.of("pe-owner", "user.test")).setEnvironment("production") @@ -97,6 +98,8 @@ public void testDomainMetaMethod() { assertFalse(dm.getAuditEnabled()); assertEquals(dm.getAccount(), "aws"); assertEquals(dm.getAzureSubscription(), "azure"); + assertEquals(dm.getAzureTenant(), "tenant"); + assertEquals(dm.getAzureClient(), "client"); assertEquals(dm.getGcpProject(), "gcp"); assertEquals(dm.getGcpProjectNumber(), "1240"); assertEquals((int) dm.getYpmId(), 10); @@ -126,7 +129,8 @@ public void testDomainMetaMethod() { .setCertDnsDomain("athenz.cloud").setMemberExpiryDays(30).setTokenExpiryMins(300) .setServiceCertExpiryMins(120).setRoleCertExpiryMins(150).setSignAlgorithm("ec") .setServiceExpiryDays(40).setUserAuthorityFilter("OnShore").setGroupExpiryDays(50) - .setAzureSubscription("azure").setGcpProject("gcp").setBusinessService("business-service") + .setAzureSubscription("azure").setAzureTenant("tenant").setAzureClient("client") + .setGcpProject("gcp").setBusinessService("business-service") .setTags(Collections.singletonMap("tagKey", new TagValueList().setList(Collections.singletonList("tagValue")))) .setMemberPurgeExpiryDays(10).setGcpProjectNumber("1240").setProductId("abcd-1234") .setFeatureFlags(3).setContacts(Map.of("pe-owner", "user.test")).setEnvironment("production") @@ -177,6 +181,20 @@ public void testDomainMetaMethod() { dm2.setAzureSubscription("azure"); assertEquals(dm, dm2); + dm2.setAzureTenant("tenant2"); + assertNotEquals(dm, dm2); + dm2.setAzureTenant(null); + assertNotEquals(dm, dm2); + dm2.setAzureTenant("tenant"); + assertEquals(dm, dm2); + + dm2.setAzureClient("client2"); + assertNotEquals(dm, dm2); + dm2.setAzureClient(null); + assertNotEquals(dm, dm2); + dm2.setAzureClient("client"); + assertEquals(dm, dm2); + dm2.setGcpProject("gcp2"); assertNotEquals(dm, dm2); dm2.setGcpProject(null); @@ -326,7 +344,8 @@ public void testTopLevelDomainMethod() { .setAuditEnabled(false).setAccount("aws").setYpmId(10).setName("testdomain").setAdminUsers(admins) .setTemplates(dtl).setApplicationId("id1").setCertDnsDomain("athenz.cloud").setMemberExpiryDays(30) .setTokenExpiryMins(300).setRoleCertExpiryMins(120).setServiceCertExpiryMins(150).setSignAlgorithm("rsa") - .setServiceExpiryDays(40).setUserAuthorityFilter("OnShore").setGroupExpiryDays(50).setAzureSubscription("azure") + .setServiceExpiryDays(40).setUserAuthorityFilter("OnShore").setGroupExpiryDays(50) + .setAzureSubscription("azure").setAzureTenant("tenant").setAzureClient("client") .setTags(Collections.singletonMap("tagKey", new TagValueList().setList(Collections.singletonList("tagValue")))) .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProject("gcp") .setGcpProjectNumber("1242").setProductId("abcd-1234").setFeatureFlags(3) @@ -342,6 +361,8 @@ public void testTopLevelDomainMethod() { assertFalse(tld.getAuditEnabled()); assertEquals(tld.getAccount(), "aws"); assertEquals(tld.getAzureSubscription(), "azure"); + assertEquals(tld.getAzureTenant(), "tenant"); + assertEquals(tld.getAzureClient(), "client"); assertEquals(tld.getGcpProject(), "gcp"); assertEquals(tld.getGcpProjectNumber(), "1242"); assertEquals((int) tld.getYpmId(), 10); @@ -372,7 +393,8 @@ public void testTopLevelDomainMethod() { .setAuditEnabled(false).setAccount("aws").setYpmId(10).setName("testdomain").setAdminUsers(admins) .setTemplates(dtl).setApplicationId("id1").setCertDnsDomain("athenz.cloud").setMemberExpiryDays(30) .setTokenExpiryMins(300).setRoleCertExpiryMins(120).setServiceCertExpiryMins(150).setSignAlgorithm("rsa") - .setServiceExpiryDays(40).setUserAuthorityFilter("OnShore").setGroupExpiryDays(50).setAzureSubscription("azure") + .setServiceExpiryDays(40).setUserAuthorityFilter("OnShore").setGroupExpiryDays(50) + .setAzureSubscription("azure").setAzureTenant("tenant").setAzureClient("client") .setTags(Collections.singletonMap("tagKey", new TagValueList().setList(Collections.singletonList("tagValue")))) .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProject("gcp") .setGcpProjectNumber("1242").setProductId("abcd-1234").setFeatureFlags(3) @@ -424,6 +446,20 @@ public void testTopLevelDomainMethod() { tld2.setAzureSubscription("azure"); assertEquals(tld, tld2); + tld2.setAzureTenant("tenant2"); + assertNotEquals(tld, tld2); + tld2.setAzureTenant(null); + assertNotEquals(tld, tld2); + tld2.setAzureTenant("tenant"); + assertEquals(tld, tld2); + + tld2.setAzureClient("client2"); + assertNotEquals(tld, tld2); + tld2.setAzureClient(null); + assertNotEquals(tld, tld2); + tld2.setAzureClient("client"); + assertEquals(tld, tld2); + tld2.setGcpProject("gcp2"); assertNotEquals(tld, tld2); tld2.setGcpProject(null); @@ -561,7 +597,8 @@ public void testSubDomainMethod() { .setParent("domain.parent").setApplicationId("101").setCertDnsDomain("athenz.cloud") .setMemberExpiryDays(30).setTokenExpiryMins(300).setServiceCertExpiryMins(120) .setRoleCertExpiryMins(150).setSignAlgorithm("rsa").setServiceExpiryDays(40) - .setUserAuthorityFilter("OnShore").setGroupExpiryDays(50).setAzureSubscription("azure") + .setUserAuthorityFilter("OnShore").setGroupExpiryDays(50) + .setAzureSubscription("azure").setAzureTenant("tenant").setAzureClient("client") .setTags(Collections.singletonMap("tagKey", new TagValueList().setList(Collections.singletonList("tagValue")))) .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProject("gcp") .setGcpProjectNumber("1244").setProductId("abcd-1234").setFeatureFlags(3) @@ -577,6 +614,8 @@ public void testSubDomainMethod() { assertFalse(sd.getAuditEnabled()); assertEquals(sd.getAccount(), "aws"); assertEquals(sd.getAzureSubscription(), "azure"); + assertEquals(sd.getAzureTenant(), "tenant"); + assertEquals(sd.getAzureClient(), "client"); assertEquals(sd.getGcpProject(), "gcp"); assertEquals(sd.getGcpProjectNumber(), "1244"); assertEquals((int) sd.getYpmId(), 10); @@ -610,7 +649,8 @@ public void testSubDomainMethod() { .setParent("domain.parent").setApplicationId("101").setCertDnsDomain("athenz.cloud") .setMemberExpiryDays(30).setTokenExpiryMins(300).setServiceCertExpiryMins(120) .setRoleCertExpiryMins(150).setSignAlgorithm("rsa").setServiceExpiryDays(40) - .setUserAuthorityFilter("OnShore").setGroupExpiryDays(50).setAzureSubscription("azure") + .setUserAuthorityFilter("OnShore").setGroupExpiryDays(50) + .setAzureSubscription("azure").setAzureTenant("tenant").setAzureClient("client") .setTags(Collections.singletonMap("tagKey", new TagValueList().setList(Collections.singletonList("tagValue")))) .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProject("gcp") .setGcpProjectNumber("1244").setProductId("abcd-1234").setFeatureFlags(3) @@ -663,6 +703,20 @@ public void testSubDomainMethod() { sd2.setAzureSubscription("azure"); assertEquals(sd, sd2); + sd2.setAzureTenant("tenant2"); + assertNotEquals(sd, sd2); + sd2.setAzureTenant(null); + assertNotEquals(sd, sd2); + sd2.setAzureTenant("tenant"); + assertEquals(sd, sd2); + + sd2.setAzureClient("client2"); + assertNotEquals(sd, sd2); + sd2.setAzureClient(null); + assertNotEquals(sd, sd2); + sd2.setAzureClient("client"); + assertEquals(sd, sd2); + sd2.setGcpProject("gcp2"); assertNotEquals(sd, sd2); sd2.setGcpProject(null); @@ -798,8 +852,9 @@ public void testUserDomainMethod() { .setTemplates(new DomainTemplateList().setTemplateNames(List.of("template"))) .setApplicationId("101").setCertDnsDomain("athenz.cloud").setMemberExpiryDays(30) .setTokenExpiryMins(300).setServiceCertExpiryMins(120).setRoleCertExpiryMins(150) - .setSignAlgorithm("rsa").setServiceExpiryDays(40).setUserAuthorityFilter("OnShore") - .setGroupExpiryDays(50).setAzureSubscription("azure").setBusinessService("business-service") + .setSignAlgorithm("rsa").setServiceExpiryDays(40).setUserAuthorityFilter("OnShore").setGroupExpiryDays(50) + .setAzureSubscription("azure").setAzureTenant("tenant").setAzureClient("client") + .setBusinessService("business-service") .setTags(Collections.singletonMap("tagKey", new TagValueList().setList(Collections.singletonList("tagValue")))) .setMemberPurgeExpiryDays(10).setGcpProject("gcp").setGcpProjectNumber("1246") .setProductId("abcd-1234").setFeatureFlags(3).setContacts(Map.of("pe-owner", "user.test")) @@ -814,6 +869,8 @@ public void testUserDomainMethod() { assertFalse(ud.getAuditEnabled()); assertEquals(ud.getAccount(), "aws"); assertEquals(ud.getAzureSubscription(), "azure"); + assertEquals(ud.getAzureTenant(), "tenant"); + assertEquals(ud.getAzureClient(), "client"); assertEquals(ud.getGcpProject(), "gcp"); assertEquals(ud.getGcpProjectNumber(), "1246"); assertEquals((int) ud.getYpmId(), 10); @@ -844,8 +901,9 @@ public void testUserDomainMethod() { .setTemplates(new DomainTemplateList().setTemplateNames(List.of("template"))) .setApplicationId("101").setCertDnsDomain("athenz.cloud").setMemberExpiryDays(30) .setTokenExpiryMins(300).setServiceCertExpiryMins(120).setRoleCertExpiryMins(150) - .setSignAlgorithm("rsa").setServiceExpiryDays(40).setUserAuthorityFilter("OnShore") - .setGroupExpiryDays(50).setAzureSubscription("azure").setBusinessService("business-service") + .setSignAlgorithm("rsa").setServiceExpiryDays(40).setUserAuthorityFilter("OnShore").setGroupExpiryDays(50) + .setAzureSubscription("azure").setAzureTenant("tenant").setAzureClient("client") + .setBusinessService("business-service") .setTags(Collections.singletonMap("tagKey", new TagValueList().setList(Collections.singletonList("tagValue")))) .setMemberPurgeExpiryDays(10).setGcpProject("gcp").setGcpProjectNumber("1246") .setProductId("abcd-1234").setFeatureFlags(3).setContacts(Map.of("pe-owner", "user.test")) @@ -896,6 +954,20 @@ public void testUserDomainMethod() { ud2.setAzureSubscription("azure"); assertEquals(ud, ud2); + ud2.setAzureTenant("tenant2"); + assertNotEquals(ud, ud2); + ud2.setAzureTenant(null); + assertNotEquals(ud, ud2); + ud2.setAzureTenant("tenant"); + assertEquals(ud, ud2); + + ud2.setAzureClient("client2"); + assertNotEquals(ud, ud2); + ud2.setAzureClient(null); + assertNotEquals(ud, ud2); + ud2.setAzureClient("client"); + assertEquals(ud, ud2); + ud2.setGcpProject("gcp2"); assertNotEquals(ud, ud2); ud2.setGcpProject(null); @@ -1051,7 +1123,8 @@ public void testDomainMethod() { .setAccount("aws").setYpmId(1).setApplicationId("101").setCertDnsDomain("athenz.cloud") .setMemberExpiryDays(30).setTokenExpiryMins(300).setServiceCertExpiryMins(120) .setRoleCertExpiryMins(150).setSignAlgorithm("rsa").setServiceExpiryDays(40) - .setUserAuthorityFilter("OnShore").setGroupExpiryDays(50).setAzureSubscription("azure") + .setUserAuthorityFilter("OnShore").setGroupExpiryDays(50) + .setAzureSubscription("azure").setAzureTenant("tenant").setAzureClient("client") .setTags(Collections.singletonMap("tagKey", new TagValueList().setList(Collections.singletonList("tagValue")))) .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProject("gcp") .setGcpProjectNumber("1237").setProductId("abcd-1234").setFeatureFlags(3) @@ -1070,6 +1143,8 @@ public void testDomainMethod() { assertTrue(d.getAuditEnabled()); assertEquals(d.getAccount(), "aws"); assertEquals(d.getAzureSubscription(), "azure"); + assertEquals(d.getAzureTenant(), "tenant"); + assertEquals(d.getAzureClient(), "client"); assertEquals(d.getGcpProject(), "gcp"); assertEquals(d.getGcpProjectNumber(), "1237"); assertEquals((int) d.getYpmId(), 1); @@ -1099,7 +1174,8 @@ public void testDomainMethod() { .setAccount("aws").setYpmId(1).setApplicationId("101").setCertDnsDomain("athenz.cloud") .setMemberExpiryDays(30).setTokenExpiryMins(300).setServiceCertExpiryMins(120) .setRoleCertExpiryMins(150).setSignAlgorithm("rsa").setServiceExpiryDays(40) - .setUserAuthorityFilter("OnShore").setGroupExpiryDays(50).setAzureSubscription("azure") + .setUserAuthorityFilter("OnShore").setGroupExpiryDays(50) + .setAzureSubscription("azure").setAzureTenant("tenant").setAzureClient("client") .setTags(Collections.singletonMap("tagKey", new TagValueList().setList(Collections.singletonList("tagValue")))) .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProject("gcp") .setGcpProjectNumber("1237").setProductId("abcd-1234").setFeatureFlags(3) @@ -1151,6 +1227,20 @@ public void testDomainMethod() { d2.setAzureSubscription("azure"); assertEquals(d, d2); + d2.setAzureTenant("tenant2"); + assertNotEquals(d, d2); + d2.setAzureTenant(null); + assertNotEquals(d, d2); + d2.setAzureTenant("tenant"); + assertEquals(d, d2); + + d2.setAzureClient("client2"); + assertNotEquals(d, d2); + d2.setAzureClient(null); + assertNotEquals(d, d2); + d2.setAzureClient("client"); + assertEquals(d, d2); + d2.setGcpProject("gcp2"); assertNotEquals(d, d2); d2.setGcpProject(null); diff --git a/core/zms/src/test/java/com/yahoo/athenz/zms/ZMSCoreTest.java b/core/zms/src/test/java/com/yahoo/athenz/zms/ZMSCoreTest.java index c3037cb5bf1..f93d1e0e547 100644 --- a/core/zms/src/test/java/com/yahoo/athenz/zms/ZMSCoreTest.java +++ b/core/zms/src/test/java/com/yahoo/athenz/zms/ZMSCoreTest.java @@ -585,9 +585,10 @@ public void testSignedDomainsMethod() { .setMemberExpiryDays(30).setServiceExpiryDays(40).setGroupExpiryDays(50) .setTokenExpiryMins(300).setRoleCertExpiryMins(120) .setServiceCertExpiryMins(150).setDescription("main domain").setOrg("org").setSignAlgorithm("rsa") - .setUserAuthorityFilter("OnShore").setGroups(gl).setAzureSubscription("azure").setGcpProject("gcp") + .setUserAuthorityFilter("OnShore").setGroups(gl) + .setAzureSubscription("azure").setAzureTenant("tenant").setAzureClient("client") .setTags(Collections.singletonMap("tagKey", new TagValueList().setList(Collections.singletonList("tagValue")))) - .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProjectNumber("1235") + .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProject("gcp").setGcpProjectNumber("1235") .setProductId("abcd-1234").setFeatureFlags(3).setContacts(Map.of("pe-owner", "user.test")) .setEnvironment("production").setResourceOwnership(new ResourceDomainOwnership().setObjectOwner("TF")); @@ -597,6 +598,8 @@ public void testSignedDomainsMethod() { assertEquals(dd.getName(), "test.domain"); assertEquals(dd.getAccount(), "aws"); assertEquals(dd.getAzureSubscription(), "azure"); + assertEquals(dd.getAzureTenant(), "tenant"); + assertEquals(dd.getAzureClient(), "client"); assertEquals(dd.getGcpProject(), "gcp"); assertEquals(dd.getGcpProjectNumber(), "1235"); assertEquals((int) dd.getYpmId(), 1); @@ -635,7 +638,8 @@ public void testSignedDomainsMethod() { .setEnabled(true).setApplicationId("101").setCertDnsDomain("athenz.cloud").setAuditEnabled(false) .setMemberExpiryDays(30).setTokenExpiryMins(300).setRoleCertExpiryMins(120).setServiceCertExpiryMins(150) .setDescription("main domain").setOrg("org").setSignAlgorithm("rsa").setServiceExpiryDays(40) - .setUserAuthorityFilter("OnShore").setGroupExpiryDays(50).setGroups(gl).setAzureSubscription("azure") + .setUserAuthorityFilter("OnShore").setGroupExpiryDays(50).setGroups(gl) + .setAzureSubscription("azure").setAzureTenant("tenant").setAzureClient("client") .setTags(Collections.singletonMap("tagKey", new TagValueList().setList(Collections.singletonList("tagValue")))) .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProject("gcp") .setGcpProjectNumber("1235").setProductId("abcd-1234").setFeatureFlags(3) @@ -697,6 +701,20 @@ public void testSignedDomainsMethod() { dd2.setAzureSubscription("azure"); assertEquals(dd, dd2); + dd2.setAzureTenant("tenant2"); + assertNotEquals(dd, dd2); + dd2.setAzureTenant(null); + assertNotEquals(dd, dd2); + dd2.setAzureTenant("tenant"); + assertEquals(dd, dd2); + + dd2.setAzureClient("client2"); + assertNotEquals(dd, dd2); + dd2.setAzureClient(null); + assertNotEquals(dd, dd2); + dd2.setAzureClient("client"); + assertEquals(dd, dd2); + dd2.setGcpProject("gcp2"); assertNotEquals(dd, dd2); dd2.setGcpProject(null); diff --git a/core/zts/src/main/java/com/yahoo/athenz/zts/DomainDetails.java b/core/zts/src/main/java/com/yahoo/athenz/zts/DomainDetails.java index 1baec44bec4..efe532739d4 100644 --- a/core/zts/src/main/java/com/yahoo/athenz/zts/DomainDetails.java +++ b/core/zts/src/main/java/com/yahoo/athenz/zts/DomainDetails.java @@ -21,6 +21,12 @@ public class DomainDetails { public String azureSubscription; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) + public String azureTenant; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public String azureClient; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_EMPTY) public String gcpProjectId; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -47,6 +53,20 @@ public DomainDetails setAzureSubscription(String azureSubscription) { public String getAzureSubscription() { return azureSubscription; } + public DomainDetails setAzureTenant(String azureTenant) { + this.azureTenant = azureTenant; + return this; + } + public String getAzureTenant() { + return azureTenant; + } + public DomainDetails setAzureClient(String azureClient) { + this.azureClient = azureClient; + return this; + } + public String getAzureClient() { + return azureClient; + } public DomainDetails setGcpProjectId(String gcpProjectId) { this.gcpProjectId = gcpProjectId; return this; @@ -78,6 +98,12 @@ public boolean equals(Object another) { if (azureSubscription == null ? a.azureSubscription != null : !azureSubscription.equals(a.azureSubscription)) { return false; } + if (azureTenant == null ? a.azureTenant != null : !azureTenant.equals(a.azureTenant)) { + return false; + } + if (azureClient == null ? a.azureClient != null : !azureClient.equals(a.azureClient)) { + return false; + } if (gcpProjectId == null ? a.gcpProjectId != null : !gcpProjectId.equals(a.gcpProjectId)) { return false; } diff --git a/core/zts/src/main/java/com/yahoo/athenz/zts/ZTSSchema.java b/core/zts/src/main/java/com/yahoo/athenz/zts/ZTSSchema.java index 9f0c547af68..92b1ed8f872 100644 --- a/core/zts/src/main/java/com/yahoo/athenz/zts/ZTSSchema.java +++ b/core/zts/src/main/java/com/yahoo/athenz/zts/ZTSSchema.java @@ -453,6 +453,8 @@ private static Schema build() { .field("name", "DomainName", false, "name of the athenz domain") .field("awsAccount", "String", true, "associated aws account id") .field("azureSubscription", "String", true, "associated azure subscription id") + .field("azureTenant", "String", true, "associated azure tenant id") + .field("azureClient", "String", true, "associated azure client id") .field("gcpProjectId", "String", true, "associated gcp project id") .field("gcpProjectNumber", "String", true, "associated gcp project number"); diff --git a/core/zts/src/main/rdl/DomainDetails.rdli b/core/zts/src/main/rdl/DomainDetails.rdli index e21d5bf468d..3cb6e1be977 100644 --- a/core/zts/src/main/rdl/DomainDetails.rdli +++ b/core/zts/src/main/rdl/DomainDetails.rdli @@ -7,6 +7,8 @@ type DomainDetails Struct { DomainName name; //name of the athenz domain String awsAccount (optional); //associated aws account id String azureSubscription (optional); //associated azure subscription id + String azureTenant (optional); //associated azure tenant id + String azureClient (optional); //associated azure client id String gcpProjectId (optional); //associated gcp project id String gcpProjectNumber (optional); //associated gcp project number } diff --git a/core/zts/src/test/java/com/yahoo/athenz/zts/DomainDetailsTest.java b/core/zts/src/test/java/com/yahoo/athenz/zts/DomainDetailsTest.java index 6fc4a540ad7..ab40e891f12 100644 --- a/core/zts/src/test/java/com/yahoo/athenz/zts/DomainDetailsTest.java +++ b/core/zts/src/test/java/com/yahoo/athenz/zts/DomainDetailsTest.java @@ -29,6 +29,8 @@ public void testDomainDetails() { .setName("athenz") .setAwsAccount("aws-account") .setAzureSubscription("azure") + .setAzureTenant("tenant") + .setAzureClient("client") .setGcpProjectId("gcp-id") .setGcpProjectNumber("gcp-number"); @@ -36,6 +38,8 @@ public void testDomainDetails() { .setName("athenz") .setAwsAccount("aws-account") .setAzureSubscription("azure") + .setAzureTenant("tenant") + .setAzureClient("client") .setGcpProjectId("gcp-id") .setGcpProjectNumber("gcp-number"); @@ -44,6 +48,8 @@ public void testDomainDetails() { assertEquals(dms1.getGcpProjectNumber(), "gcp-number"); assertEquals(dms1.getName(), "athenz"); assertEquals(dms1.getAzureSubscription(), "azure"); + assertEquals(dms1.getAzureTenant(), "tenant"); + assertEquals(dms1.getAzureClient(), "client"); assertEquals(dms2, dms1); assertEquals(dms2, dms2); @@ -70,6 +76,20 @@ public void testDomainDetails() { dms2.setAzureSubscription("azure"); assertEquals(dms2, dms1); + dms2.setAzureTenant("tenant2"); + assertNotEquals(dms2, dms1); + dms2.setAzureTenant(null); + assertNotEquals(dms2, dms1); + dms2.setAzureTenant("tenant"); + assertEquals(dms2, dms1); + + dms2.setAzureClient("client2"); + assertNotEquals(dms2, dms1); + dms2.setAzureClient(null); + assertNotEquals(dms2, dms1); + dms2.setAzureClient("client"); + assertEquals(dms2, dms1); + dms2.setGcpProjectId("gcp2"); assertNotEquals(dms2, dms1); dms2.setGcpProjectId(null); diff --git a/docs/design/azure_identity_access_tokens.md b/docs/design/azure_identity_access_tokens.md new file mode 100644 index 00000000000..a3af35af22b --- /dev/null +++ b/docs/design/azure_identity_access_tokens.md @@ -0,0 +1,142 @@ +# Azure Identity Access Tokens + +Integration with Azure's user managed identities serves two purposes in Athenz: + +1. Athenz users can obtain access tokens for Azure APIs, for their Azure identities, + through ZTS and by being part of designated Athenz roles. +1. ZTS can access Azure APIs to run an instance provider component internally, + supporting any number of tenants with an Azure subscription in an easy manner. + + +## Design + +Azure allows its users to exchange ID tokens signed by a designated issuer for access tokens +for a configured user managed identity: +[Azure reference documentation](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#third-case-access-token-request-with-a-federated-credential). +This API for federated identities is a bit limited; only the `iss`, `sub` and `aud` claims are relevant: + +1. The designated issuer must match the `iss` claim. +1. The `aud` is encouraged to be `api://AzureADTokenExchange`. +1. This means the issuer must use the `sub` claim to differentiate between exchange ID tokens for different Azure identities. + +As the main goal here is to allow Athenz users to translate their Athenz credentials into Azure access tokens, +and we want to use Athenz role membership to control who has access to what federated Azure identities, we propose +the following configuration of the federated identity credentials in Azure: + +1. ZTS issues ID tokens with its configured OAuth issuer in the `iss` claim. +1. Each Azure identity is linked to a single Athenz role; this link is established by using the full role ARN, e.g., + `coretech:role.azure-client`, as the expected `sub` of the exchange tokens. +1. ZTS uses the suggested `api://AzureADTokenExchange` for the `aud` claim. + +We also want the Athenz users to be able to obtain credentials for an Azure identity by its _name_ and enclosing _resource group_, +rather than the UUID-type _client ID_ of the credential. This requires ZTS to have read access to the user managed identities of +the user's Azure subscription, which we solve by configuring a dedicated "Athenz Azure client" identity in the Azure subscription, +with the required read privileges. Giving this identity read privileges to VM instance metadata also allows ZTS to run the Azure +instance provider (below), which is easier than setting up a webserver which runs the provider code within each Azure subscription. + + +### Required setup + +To enable the Azure integration, there is some configuration on the Athenz side for the Athenz system administrators. +Users must do some setup in both Athenz and Azure for onboarding, and also for each new Azure identity to assume. + + +#### Athenz + +Globally, for the whole Athenz system: + +1. Create a system role `athenz.azure:role.azure-client`. This represents ZTS, and will allow access to Azure APIs needed to + verify VM instance identity requests and look up Azure identities. + 1. Ensure all instance providers are members of this role, so they can read VM data. + 1. The Azure Access Token Provider obtains ID tokens for this role without memership checks. +1. Required configuration for the ZTS server (which also runs the instance providers): + 1. `athenz.zts.external_creds_providers=gcp,azure` (Azure is not enabled by default.) + 1. `athenz.zts.oauth_issuer=` + 1. `athenz.zts.azure_resource_uri=api://` + 1. `athenz.zts.azure_dns_suffix=...` (System-specific.) + +For each domain that uses Azure as a cloud provider: + +1. Specify Azure _subscription_, _tenant_ and _client_ on the domain; these are system meta attributes, see the below RDL changes for details. +1. For each user managed Azure identity to assume, create a designated role under the domain. Members of this role will be + able to acquire an access token for the linked Azure identity from ZTS, for configured scope(s): + 1. Create a policy `ALLOW azure.scope_access to on `, e.g. allow the linked role default scope access: + `ALLOW azure.scope_access to azure-log-reader on https://management.azure.com/.default`. + 1. Not implemented, but a suggestion for later: + Create a policy `ALLOW azure.assume_identity to XXX on .` + that can be used by ZMS to list accessible identities for a user, like for AWS and GCP. + + +#### Azure + +For each Azure tenant: + +1. Create the "Athenz Azure client" user managed identity, which ZTS assumes when reading data (VMs and user managed identities, see above): + 1. Add a federated credential which allows ZTS to assume the identity with: + 1. issuer: `` + 1. subject: `athenz.azure:role.azure-client` + 1. audience: `api://AzureADTokenExchange` + 1. Create and assign it a role with permissions: + 1. `Microsoft.ManagedIdentity/userAssignedIdentities/read` + 1. `Microsoft.Compute/virtualMachines/read` + 1. Note the ID of the created identity, and register it on the corresponding Athenz domain, together with the tenant and subscription IDs (see above). +1. Create an app registration to use as the token audience for VM metadata (required configuration for the SIA agent on Azure VMs), with: + 1. sign-in-audience: `AzureADMultipleOrgs` + 1. identifier-uris: `api://` +1. Set up additional user managed identities with custom roles, as required: + 1. Add a federated credential which allows members of the designated Athenz role to assume the identity: + 1. issuer: `` + 1. subject: `:role.` + 1. audience: `api://AzureADTokenExchange` + 1. Note the resource group and name of the identity; these are used when obtaining access tokens through Athenz, see below. + +Multiple subscriptions within the same tenant can share the same client setup. + + +### RDL Struct Updates + +`DomainMeta` has two new system meta attribute fields `azureTenant` and `azureClient`: + +```rdl +type DomainMeta Struct { + ... + + String azureSubscription (optional); //associated azure subscription id (system attribute - uniqueness check - if enabled) + String azureTenant (optional); //associated azure tenant id (system attribute) + String azureClient (optional); //associated azure client id (system attribute) + ... +} +``` + + +### API Changes + + +#### Configuring domain for Azure + +The new `DomainMeta` fields are updated like `azureSubscription`, through `PUT "/domain/{name}/meta/system/{attribute}"`. +The payload must now contain all three fields (above)—not just the `azureSubscription`. + + +#### Obtaining Azure access tokens + +To get an access token for the example user managed identity `log-reader` in the Azure resource group `system`, associated with +the Athenz role `azure-log-reader` under the Athenz domain `coretech`, simply do: + +``` +POST /external/azure/coretech/creds +{ + "clientId": "coretech.azure", + "attributes": { + "athenzRole": "azure-log-reader", + "azureResourceGroup": "system", + "azureClientName": "log-reader", + “azureScope": + } +} +``` + +**Note 1:** The :clientId: should be `.azure`, although it is not really used for anything. This was done to match the GCP setup. + +**Note 2:** It is also possible to specify `"azureClientId"` instead of `"azureResourceGroup"` and `"azureClientName"`. When this is specified, +ZTS skips the client ID lookup, and uses the supplied value instead. diff --git a/libs/go/zmscli/cli.go b/libs/go/zmscli/cli.go index 59e83a9a641..f954872a8e3 100644 --- a/libs/go/zmscli/cli.go +++ b/libs/go/zmscli/cli.go @@ -856,8 +856,8 @@ func (cli Zms) EvalCommand(params []string) (*string, error) { return cli.SetDomainAccount(dn, args[0]) } case "set-azure-subscription", "set-domain-subscription": - if argc == 1 { - return cli.SetDomainSubscription(dn, args[0]) + if argc == 3 { + return cli.SetDomainSubscription(dn, args[0], args[1], args[2]) } case "set-gcp-project", "set-domain-project": if argc == 2 { @@ -1540,14 +1540,16 @@ func (cli Zms) HelpSpecificCommand(interactive bool, cmd string) string { buf.WriteString(" " + domainExample + " set-aws-account \"134901934383\"\n") case "set-azure-subscription", "set-domain-subscription": buf.WriteString(" syntax:\n") - buf.WriteString(" [-o json] " + domainParam + " set-azure-subscription subscription-id\n") + buf.WriteString(" [-o json] " + domainParam + " set-azure-subscription subscription-id tenant-id client-id\n") buf.WriteString(" parameters:\n") if !interactive { buf.WriteString(" domain : name of the domain being updated\n") } buf.WriteString(" subscription-id : set the azure subscription id for the domain\n") + buf.WriteString(" tenant-id : set the azure tenant id for the domain\n") + buf.WriteString(" client-id : set the azure client id for the domain\n") buf.WriteString(" examples:\n") - buf.WriteString(" " + domainExample + " set-azure-subscription \"12345678-1234-1234-1234-1234567890\"\n") + buf.WriteString(" " + domainExample + " set-azure-subscription \"12345678-1234-1234-1234-1234567890\" \"87654321-4321-5678-4321-5678434321\" \"87654321-1234-5678-1234-5678432109\"\n") case "set-gcp-project", "set-domain-project": buf.WriteString(" syntax:\n") buf.WriteString(" [-o json] " + domainParam + " set-gcp-project project-id project-number\n") diff --git a/libs/go/zmscli/domain.go b/libs/go/zmscli/domain.go index b5d8c6d6c2a..c83eca43b16 100644 --- a/libs/go/zmscli/domain.go +++ b/libs/go/zmscli/domain.go @@ -1118,9 +1118,11 @@ func (cli Zms) SetDomainAccount(dn string, account string) (*string, error) { return cli.dumpByFormat(message, cli.buildYAMLOutput) } -func (cli Zms) SetDomainSubscription(dn string, subscription string) (*string, error) { +func (cli Zms) SetDomainSubscription(dn, subscription, tenant, client string) (*string, error) { meta := zms.DomainMeta{ AzureSubscription: subscription, + AzureTenant: tenant, + AzureClient: client, } err := cli.Zms.PutDomainSystemMeta(zms.DomainName(dn), "azuresubscription", cli.AuditRef, &meta) if err != nil { diff --git a/libs/go/zmscli/dump.go b/libs/go/zmscli/dump.go index 945274e3cd3..ab8bf84a1cf 100644 --- a/libs/go/zmscli/dump.go +++ b/libs/go/zmscli/dump.go @@ -28,6 +28,8 @@ func (cli Zms) dumpDomain(buf *bytes.Buffer, domain *zms.Domain) { dumpStringValue(buf, indentLevel1, "description", domain.Description) dumpStringValue(buf, indentLevel1, "aws_account", domain.Account) dumpStringValue(buf, indentLevel1, "azure_subscription", domain.AzureSubscription) + dumpStringValue(buf, indentLevel1, "azure_tenant", domain.AzureTenant) + dumpStringValue(buf, indentLevel1, "azure_client", domain.AzureClient) dumpStringValue(buf, indentLevel1, "gcp_project", domain.GcpProject) dumpStringValue(buf, indentLevel1, "gcp_project_number", domain.GcpProjectNumber) dumpStringValue(buf, indentLevel1, "application_id", domain.ApplicationId) @@ -566,6 +568,18 @@ func (cli Zms) dumpSignedDomain(buf *bytes.Buffer, signedDomain *zms.SignedDomai buf.WriteString(domainData.AzureSubscription) buf.WriteString("\n") } + if domainData.AzureTenant != "" { + buf.WriteString(indentLevel1) + buf.WriteString("azure_tenant: ") + buf.WriteString(domainData.AzureTenant) + buf.WriteString("\n") + } + if domainData.AzureClient != "" { + buf.WriteString(indentLevel1) + buf.WriteString("azure_client: ") + buf.WriteString(domainData.AzureClient) + buf.WriteString("\n") + } if domainData.GcpProject != "" { buf.WriteString(indentLevel1) buf.WriteString("gcp_project: ") diff --git a/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/InstanceProvider.java b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/InstanceProvider.java index d0f166f92b0..1e851f11031 100644 --- a/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/InstanceProvider.java +++ b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/InstanceProvider.java @@ -65,6 +65,8 @@ public interface InstanceProvider { String ZTS_INSTANCE_PRIVATE_IP = "instancePrivateIp"; String ZTS_INSTANCE_AWS_ACCOUNT = "awsAccount"; String ZTS_INSTANCE_AZURE_SUBSCRIPTION = "azureSubscription"; + String ZTS_INSTANCE_AZURE_TENANT = "azureTenant"; + String ZTS_INSTANCE_AZURE_CLIENT = "azureClient"; String ZTS_INSTANCE_GCP_PROJECT = "gcpProject"; String ZTS_INSTANCE_CERT_HOSTNAME = "certHostname"; String ZTS_INSTANCE_CERT_RSA_MOD_HASH = "certRsaModHash"; diff --git a/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/AzureVmIdentity.java b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/AzureVmIdentity.java index a0737e01326..081639f7d15 100644 --- a/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/AzureVmIdentity.java +++ b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/AzureVmIdentity.java @@ -15,9 +15,12 @@ */ package com.yahoo.athenz.instance.provider.impl; +import java.util.Map; + public class AzureVmIdentity { private String principalId; + private Map userAssignedIdentities; private String tenantId; public String getPrincipalId() { @@ -28,6 +31,14 @@ public void setPrincipalId(String principalId) { this.principalId = principalId; } + public Map getUserAssignedIdentities() { + return userAssignedIdentities; + } + + public void setUserAssignedIdentities(Map userAssignedIdentities) { + this.userAssignedIdentities = userAssignedIdentities; + } + public String getTenantId() { return tenantId; } diff --git a/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/AzureVmUserManagedIdentity.java b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/AzureVmUserManagedIdentity.java new file mode 100644 index 00000000000..88868aec1e7 --- /dev/null +++ b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/AzureVmUserManagedIdentity.java @@ -0,0 +1,30 @@ +/* + * Copyright The Athenz Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yahoo.athenz.instance.provider.impl; + +public class AzureVmUserManagedIdentity { + + private String principalId; + + public String getPrincipalId() { + return principalId; + } + + public void setPrincipalId(String principalId) { + this.principalId = principalId; + } + +} diff --git a/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceAzureProvider.java b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceAzureProvider.java index 4ce65f557a9..9e164b33201 100644 --- a/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceAzureProvider.java +++ b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceAzureProvider.java @@ -19,23 +19,25 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.yahoo.athenz.auth.KeyStore; import com.yahoo.athenz.auth.token.OAuth2Token; -import com.yahoo.athenz.auth.token.jwts.JwtsSigningKeyResolver; import com.yahoo.athenz.auth.token.jwts.JwtsHelper; +import com.yahoo.athenz.auth.token.jwts.JwtsSigningKeyResolver; import com.yahoo.athenz.common.server.http.HttpDriver; +import com.yahoo.athenz.instance.provider.ExternalCredentialsProvider; import com.yahoo.athenz.instance.provider.InstanceConfirmation; import com.yahoo.athenz.instance.provider.InstanceProvider; import com.yahoo.athenz.instance.provider.ResourceException; -import com.yahoo.athenz.zts.AccessTokenResponse; +import com.yahoo.athenz.zts.ExternalCredentialsRequest; import org.eclipse.jetty.util.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.SSLContext; -import java.io.IOException; -import java.util.*; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; public class InstanceAzureProvider implements InstanceProvider { @@ -47,10 +49,7 @@ public class InstanceAzureProvider implements InstanceProvider { static final String AZURE_PROP_ZTS_RESOURCE_URI = "athenz.zts.azure_resource_uri"; static final String AZURE_PROP_DNS_SUFFIX = "athenz.zts.azure_dns_suffix"; static final String AZURE_PROP_OPENID_CONFIG_URI = "athenz.zts.azure_openid_config_uri"; - static final String AZURE_PROP_TOKEN_UPDATE_TIMEOUT = "athenz.zts.azure_token_update_timeout"; - static final String AZURE_PROP_MGMT_BASE_URI = "athenz.zts.azure_mgmt_base_uri"; - static final String AZURE_PROP_META_BASE_URI = "athenz.zts.azure_meta_base_uri"; static final String AZURE_PROP_MGMT_MAX_POOL_ROUTE = "athenz.zts.azure_mgmt_client_max_pool_route"; static final String AZURE_PROP_MGMT_MAX_POOL_TOTAL = "athenz.zts.azure_mgmt_client_max_pool_total"; static final String AZURE_PROP_MGMT_RETRY_INTERVAL_MS = "athenz.zts.azure_mgmt_client_retry_interval_ms"; @@ -58,7 +57,12 @@ public class InstanceAzureProvider implements InstanceProvider { static final String AZURE_PROP_MGMT_CONNECT_TIMEOUT_MS = "athenz.zts.azure_mgmt_client_connect_timeout_ms"; static final String AZURE_PROP_MGMT_READ_TIMEOUT_MS = "athenz.zts.azure_mgmt_client_read_timeout_ms"; - static final String AZURE_OPENID_CONFIG_URI = "https://login.microsoftonline.com/common/.well-known/openid-configuration"; + static final String AZURE_MGMT_BASE_URI = "https://management.azure.com"; + static final String AZURE_OPENID_BASE_URI = "https://login.microsoftonline.com"; + static final String AZURE_OPENID_CONFIG_URI = AZURE_OPENID_BASE_URI + "/common/.well-known/openid-configuration"; + + static final String ATHENZ_AZURE_CLIENT_ID = "athenz.azure.azure-client"; + static final String ATHENZ_AZURE_CLIENT_SCOPE = "openid athenz.azure:role.azure-client"; String azureProvider = null; Set dnsSuffixes = null; @@ -68,33 +72,31 @@ public class InstanceAzureProvider implements InstanceProvider { ObjectMapper jsonMapper = null; String ztsResourceUri = null; JwtsSigningKeyResolver signingKeyResolver = null; - String azureMgmtBaseUri = null; - String azureMetaBaseUri = null; - String accessToken = null; - ScheduledExecutorService scheduledThreadPool = null; + ExternalCredentialsProvider externalCredentialsProvider = null; @Override public Scheme getProviderScheme() { return Scheme.HTTP; } + @Override + public void setExternalCredentialsProvider(ExternalCredentialsProvider externalCredentialsProvider) { + this.externalCredentialsProvider = externalCredentialsProvider; + } + @Override public void initialize(String provider, String providerEndpoint, SSLContext sslContext, KeyStore keyStore) { azureProvider = System.getProperty(AZURE_PROP_PROVIDER); - azureMgmtBaseUri = System.getProperty(AZURE_PROP_MGMT_BASE_URI, "https://management.azure.com"); - azureMetaBaseUri = System.getProperty(AZURE_PROP_META_BASE_URI, "http://169.254.169.254"); // we need to extract Azure jwks uri and initialize our jwks signer - boolean enabled = true; final String openIdConfigUri = System.getProperty(AZURE_PROP_OPENID_CONFIG_URI, AZURE_OPENID_CONFIG_URI); JwtsHelper helper = new JwtsHelper(); azureJwksUri = helper.extractJwksUri(openIdConfigUri, sslContext); if (StringUtil.isEmpty(azureJwksUri)) { LOGGER.error("Azure jwks uri not available - no instance requests will be authorized"); - enabled = false; } signingKeyResolver = new JwtsSigningKeyResolver(azureJwksUri, sslContext, true); @@ -103,7 +105,6 @@ public void initialize(String provider, String providerEndpoint, SSLContext sslC if (signingKeyResolver.publicKeyCount() == 0) { LOGGER.error("No Azure public keys available - no instance requests will be authorized"); - enabled = false; } // determine the dns suffix. if this is not specified we'll @@ -113,7 +114,6 @@ public void initialize(String provider, String providerEndpoint, SSLContext sslC final String dnsSuffix = System.getProperty(AZURE_PROP_DNS_SUFFIX); if (StringUtil.isEmpty(dnsSuffix)) { LOGGER.error("Azure Suffix not specified - no instance requests will be authorized"); - enabled = false; } else { dnsSuffixes.addAll(Arrays.asList(dnsSuffix.split(","))); } @@ -123,7 +123,6 @@ public void initialize(String provider, String providerEndpoint, SSLContext sslC ztsResourceUri = System.getProperty(AZURE_PROP_ZTS_RESOURCE_URI); if (StringUtil.isEmpty(ztsResourceUri)) { LOGGER.error("Azure ZTS Resource URI not specified - no instance requests will be authorized"); - enabled = false; } // get our json deserializer @@ -138,39 +137,6 @@ public void initialize(String provider, String providerEndpoint, SSLContext sslC } catch (Exception ex) { LOGGER.error("Azure HTTP Client not created - no instance requests will be authorized"); httpDriver = null; - enabled = false; - } - - // if our settings are not valid there is no point of starting - // our timer thread to obtain AD access token - - if (enabled) { - - // let's first manually fetch the access token - - try { - fetchAccessToken(); - } catch (Exception ex) { - LOGGER.error("Unable to fetch VM access token", ex); - } - - // now setup our credential updater - - int credsUpdateTime = Integer.parseInt(System.getProperty(AZURE_PROP_TOKEN_UPDATE_TIMEOUT, "10")); - - scheduledThreadPool = Executors.newScheduledThreadPool(1); - scheduledThreadPool.scheduleAtFixedRate(new AzureCredentialsUpdater(), credsUpdateTime, - credsUpdateTime, TimeUnit.MINUTES); - } - } - - @Override - public void close() { - if (httpDriver != null) { - httpDriver.close(); - } - if (scheduledThreadPool != null) { - scheduledThreadPool.shutdown(); } } @@ -186,6 +152,10 @@ public ResourceException error(int errorCode, String message) { @Override public InstanceConfirmation confirmInstance(InstanceConfirmation confirmation) { + if (externalCredentialsProvider == null) { + throw error("External credentials provider must be configured for the Azure provider"); + } + AzureAttestationData info; try { info = jsonMapper.readValue(confirmation.getAttestationData(), AzureAttestationData.class); @@ -229,7 +199,7 @@ public InstanceConfirmation confirmInstance(InstanceConfirmation confirmation) { // verify that access token in the request is valid - if (!verifyInstanceIdentity(info, confirmation.getProvider(), serviceName, instanceId.toString())) { + if (!verifyInstanceIdentity(info, confirmation.getProvider(), serviceName, instanceDomain, instanceId.toString())) { throw error("Unable to verify instance identity credentials"); } @@ -237,7 +207,7 @@ public InstanceConfirmation confirmInstance(InstanceConfirmation confirmation) { } boolean verifyInstanceIdentity(AzureAttestationData info, final String provider, final String serviceName, - final String instanceId) { + final String domain, final String instanceId) { // first validate the access token provided by the client @@ -254,7 +224,7 @@ boolean verifyInstanceIdentity(AzureAttestationData info, final String provider, // now fetch the details and verify object id - final String vmDetailsData = fetchVMDetails(info); + final String vmDetailsData = fetchVMDetails(info, domain); if (vmDetailsData == null) { LOGGER.error("Unable to fetch VM details for account {}", info.getSubscriptionId()); return false; @@ -266,11 +236,20 @@ boolean verifyInstanceIdentity(AzureAttestationData info, final String provider, return false; } - // the vm identity id must match our token subject + // one vm identity id must match our token subject + Set identities = new HashSet<>(); + if (vmDetails.getIdentity().getPrincipalId() != null) { + identities.add(vmDetails.getIdentity().getPrincipalId()); + } + if (vmDetails.getIdentity().getUserAssignedIdentities() != null) { + for (AzureVmUserManagedIdentity managedIdentity : vmDetails.getIdentity().getUserAssignedIdentities().values()) { + identities.add(managedIdentity.getPrincipalId()); + } + } - if (!vmToken.getSubject().equals(vmDetails.getIdentity().getPrincipalId())) { + if (!identities.contains(vmToken.getSubject())) { LOGGER.error("Azure Token not issued for requested VM instance {}/{}", - vmToken.getSubject(), vmDetails.getIdentity().getPrincipalId()); + vmToken.getSubject(), String.join(",", identities)); return false; } @@ -308,13 +287,20 @@ AzureVmDetails parseVmDetails(String vmDetailsData) { return null; } - String fetchVMDetails(AzureAttestationData info) { + String fetchVMDetails(AzureAttestationData info, String domain) { if (httpDriver == null) { LOGGER.error("No Azure HTTP Client available"); return null; } + ExternalCredentialsRequest request = new ExternalCredentialsRequest(); + request.setClientId(ATHENZ_AZURE_CLIENT_ID); + Map attributes = new HashMap<>(); + attributes.put("athenzScope", ATHENZ_AZURE_CLIENT_SCOPE); + request.setAttributes(attributes); + String accessToken = externalCredentialsProvider.getExternalCredentials("azure", domain, request).getAttributes().get("accessToken"); + if (accessToken == null) { LOGGER.error("No authorization access token available"); return null; @@ -322,13 +308,13 @@ String fetchVMDetails(AzureAttestationData info) { // extract the VM details from Azure Management API - final String vmUri = azureMgmtBaseUri + "/subscriptions/" + info.getSubscriptionId() + + final String vmUri = AZURE_MGMT_BASE_URI + "/subscriptions/" + info.getSubscriptionId() + "/resourceGroups/" + info.getResourceGroupName() + "/providers/Microsoft.Compute/virtualMachines/" + info.getName() + "?api-version=2020-06-01"; Map headers = new HashMap<>(); - headers.put("Authorization", accessToken); + headers.put("Authorization", "Bearer " + accessToken); try { return httpDriver.doGet(vmUri, headers); @@ -380,52 +366,4 @@ private HttpDriver getHttpDriver(SSLContext sslContext) { .build(); } - void fetchAccessToken() throws IOException { - // extract the VM details from Azure Management API - - final String tokenUri = azureMetaBaseUri + "/metadata/identity/oauth2/token?api-version=2020-06-01&resource=" + - azureMgmtBaseUri; - - Map headers = new HashMap<>(); - headers.put("Metadata", "true"); - - String tokenData = httpDriver.doGet(tokenUri, headers); - if (tokenData == null) { - throw error("Unable to fetch access token"); - } - - AccessTokenResponse tokenResponse = null; - try { - tokenResponse = jsonMapper.readValue(tokenData, AccessTokenResponse.class); - } catch (Exception ex) { - LOGGER.error("unable to parse access token response: {}", tokenData, ex); - } - - if (tokenResponse == null) { - throw error("Unable to parse access token response"); - } - - if (StringUtil.isEmpty(tokenResponse.access_token)) { - throw error("Empty access token returned"); - } - - accessToken = "Bearer " + tokenResponse.access_token; - } - - class AzureCredentialsUpdater implements Runnable { - - @Override - public void run() { - - LOGGER.info("AzureCredentialsUpdater: Starting Azure credentials updater task..."); - - try { - fetchAccessToken(); - } catch (Exception ex) { - LOGGER.error("AzureCredentialsUpdater: unable to fetch Azure access token", ex); - } - - LOGGER.info("AzureCredentialsUpdater: Azure credentials updater task completed"); - } - } } diff --git a/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceAzureProviderTest.java b/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceAzureProviderTest.java index 7e79a9948ef..77ada9857d7 100644 --- a/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceAzureProviderTest.java +++ b/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceAzureProviderTest.java @@ -18,9 +18,11 @@ import com.yahoo.athenz.auth.token.AccessToken; import com.yahoo.athenz.auth.util.Crypto; import com.yahoo.athenz.common.server.http.HttpDriver; +import com.yahoo.athenz.instance.provider.ExternalCredentialsProvider; import com.yahoo.athenz.instance.provider.InstanceConfirmation; import com.yahoo.athenz.instance.provider.InstanceProvider; import com.yahoo.athenz.instance.provider.ResourceException; +import com.yahoo.athenz.zts.ExternalCredentialsResponse; import io.jsonwebtoken.SignatureAlgorithm; import org.mockito.Mockito; import org.testng.annotations.AfterMethod; @@ -37,7 +39,14 @@ import java.util.Map; import static org.mockito.ArgumentMatchers.any; -import static org.testng.Assert.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.matches; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; public class InstanceAzureProviderTest { @@ -58,6 +67,15 @@ public void shutdown() { System.clearProperty(InstanceAzureProvider.AZURE_PROP_MGMT_CONNECT_TIMEOUT_MS); } + private void setUpExternalCredentialsProvider(InstanceAzureProvider provider) { + ExternalCredentialsProvider credentialsProvider = Mockito.mock(ExternalCredentialsProvider.class); + provider.setExternalCredentialsProvider(credentialsProvider); + ExternalCredentialsResponse response = new ExternalCredentialsResponse(); + response.setAttributes(new HashMap<>()); + response.getAttributes().put("accessToken", "access-token"); + Mockito.when(credentialsProvider.getExternalCredentials(any(), any(), any())).thenReturn(response); + } + @Test public void testInitializeDefaults() throws IOException { @@ -65,6 +83,7 @@ public void testInitializeDefaults() throws IOException { System.setProperty(InstanceAzureProvider.AZURE_PROP_OPENID_CONFIG_URI, "file://" + configUri.getCanonicalPath()); InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); assertEquals(provider.getProviderScheme(), InstanceProvider.Scheme.HTTP); @@ -83,6 +102,7 @@ public void testInitializeEmptyValues() throws IOException { System.clearProperty(InstanceAzureProvider.AZURE_PROP_DNS_SUFFIX); InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); assertTrue(provider.dnsSuffixes.isEmpty()); @@ -104,6 +124,16 @@ public void testConfirmInstance() throws IOException { InstanceAzureProvider provider = new InstanceAzureProvider(); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); + ExternalCredentialsResponse credentialsResponse = new ExternalCredentialsResponse(); + credentialsResponse.setAttributes(new HashMap<>()); + credentialsResponse.getAttributes().put("accessToken", "access-token"); + provider.externalCredentialsProvider = Mockito.mock(ExternalCredentialsProvider.class); + Mockito.when(provider.externalCredentialsProvider.getExternalCredentials(eq("azure"), eq("athenz"), argThat(arg -> { + return arg.getClientId().equals("athenz.azure.azure-client") && + arg.getAttributes().get("athenzScope").equals("openid athenz.azure:role.azure-client") && + arg.getAttributes().size() == 1; + }))).thenReturn(credentialsResponse); + InstanceConfirmation confirmation = new InstanceConfirmation(); confirmation.setDomain("athenz"); confirmation.setService("backend"); @@ -128,8 +158,6 @@ public void testConfirmInstance() throws IOException { provider.signingKeyResolver.addPublicKey("eckey1", publicKey); provider.httpDriver = setupHttpDriver(); - InstanceAzureProvider.AzureCredentialsUpdater updater = provider.new AzureCredentialsUpdater(); - updater.run(); InstanceConfirmation providerConfirm = provider.confirmInstance(confirmation); assertNotNull(providerConfirm); @@ -152,6 +180,7 @@ public void testConfirmInstanceProviderConfig() throws IOException { System.setProperty(InstanceAzureProvider.AZURE_PROP_ZTS_RESOURCE_URI, "https://azure-zts"); System.setProperty(InstanceAzureProvider.AZURE_PROP_OPENID_CONFIG_URI, "file://" + configFile.getCanonicalPath()); InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); InstanceConfirmation confirmation = new InstanceConfirmation(); @@ -178,8 +207,6 @@ public void testConfirmInstanceProviderConfig() throws IOException { provider.signingKeyResolver.addPublicKey("eckey1", publicKey); provider.httpDriver = setupHttpDriver(); - InstanceAzureProvider.AzureCredentialsUpdater updater = provider.new AzureCredentialsUpdater(); - updater.run(); InstanceConfirmation providerConfirm = provider.confirmInstance(confirmation); assertNotNull(providerConfirm); @@ -202,6 +229,7 @@ public void testRefreshInstance() throws IOException { System.setProperty(InstanceAzureProvider.AZURE_PROP_ZTS_RESOURCE_URI, "https://azure-zts"); System.setProperty(InstanceAzureProvider.AZURE_PROP_OPENID_CONFIG_URI, "file://" + configFile.getCanonicalPath()); InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); InstanceConfirmation confirmation = new InstanceConfirmation(); @@ -227,8 +255,31 @@ public void testRefreshInstance() throws IOException { PublicKey publicKey = Crypto.loadPublicKey(ecPublicKey); provider.signingKeyResolver.addPublicKey("eckey1", publicKey); - provider.httpDriver = setupHttpDriver(); - provider.fetchAccessToken(); + + String vmDetailsWithUserAssignedIdentities = + "{\n" + + " \"name\": \"athenz-client\",\n" + + " \"id\": \"/subscriptions/123456/resourceGroups/Athenz/providers/Microsoft.Compute/virtualMachines/athenz-client\",\n" + + " \"location\": \"westus2\",\n" + + " \"tags\": {\n" + + " \"athenz\": \"athenz.backend\"\n" + + " },\n" + + " \"identity\": {\n" + + " \"type\": \"UserAssigned\",\n" + + " \"userAssignedIdentities\": {\n" + + " \"/subscriptions/23423423-d46a-45db-aad6-29a1fdab4f86/resourceGroups/system/providers/Microsoft.ManagedIdentity/userAssignedIdentities/my-id\": {\n" + + " \"principalId\": \"111111-2222-3333-4444-555555555\",\n" + + " \"clientId\": \"f6ed0c62-f2cb-4ebc-8c4e-e81c43887914\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"properties\": {\n" + + " \"vmId\": \"2222-3333\"\n" + + " }\n" + + "}"; + + + provider.httpDriver = setupHttpDriver(vmDetailsWithUserAssignedIdentities); InstanceConfirmation providerConfirm = provider.refreshInstance(confirmation); assertNotNull(providerConfirm); @@ -241,26 +292,29 @@ public void testRefreshInstance() throws IOException { } private HttpDriver setupHttpDriver() throws IOException { - - HttpDriver httpDriver = Mockito.mock(HttpDriver.class); - final String vmDetails = "{\n" + - " \"name\": \"athenz-client\",\n" + - " \"id\": \"/subscriptions/123456/resourceGroups/Athenz/providers/Microsoft.Compute/virtualMachines/athenz-client\",\n" + - " \"location\": \"westus2\",\n" + - " \"tags\": {\n" + - " \"athenz\": \"athenz.backend\"\n" + - " },\n" + - " \"identity\": {\n" + - " \"type\": \"SystemAssigned, UserAssigned\",\n" + - " \"principalId\": \"111111-2222-3333-4444-555555555\",\n" + - " \"tenantId\": \"222222-3333-4444-5555-66666666\"\n" + - " },\n" + - " \"properties\": {\n" + - " \"vmId\": \"2222-3333\"\n" + - " }\n" + - "}"; + " \"name\": \"athenz-client\",\n" + + " \"id\": \"/subscriptions/123456/resourceGroups/Athenz/providers/Microsoft.Compute/virtualMachines/athenz-client\",\n" + + " \"location\": \"westus2\",\n" + + " \"tags\": {\n" + + " \"athenz\": \"athenz.backend\"\n" + + " },\n" + + " \"identity\": {\n" + + " \"type\": \"SystemAssigned, UserAssigned\",\n" + + " \"principalId\": \"111111-2222-3333-4444-555555555\",\n" + + " \"tenantId\": \"222222-3333-4444-5555-66666666\"\n" + + " },\n" + + " \"properties\": {\n" + + " \"vmId\": \"2222-3333\"\n" + + " }\n" + + "}"; + return setupHttpDriver(vmDetails); + } + + private HttpDriver setupHttpDriver(String vmDetails) throws IOException { + + HttpDriver httpDriver = Mockito.mock(HttpDriver.class); final String vmUri = "https://management.azure.com/subscriptions/1111-2222/resourceGroups/prod" + "/providers/Microsoft.Compute/virtualMachines/athenz-client?api-version=2020-06-01"; @@ -269,17 +323,6 @@ private HttpDriver setupHttpDriver() throws IOException { vmHeaders.put("Authorization", "Bearer access-token"); Mockito.when(httpDriver.doGet(vmUri, vmHeaders)).thenReturn(vmDetails); - final String tokenDetails = - "{\n" + - " \"access_token\": \"access-token\"" + - "}"; - - Map tokenHeaders = new HashMap<>(); - tokenHeaders.put("Metadata", "true"); - final String tokenUri = "http://169.254.169.254/metadata/identity/oauth2/token" + - "?api-version=2020-06-01&resource=https://management.azure.com"; - Mockito.when(httpDriver.doGet(tokenUri, tokenHeaders)).thenReturn(tokenDetails); - return httpDriver; } @@ -287,6 +330,7 @@ private HttpDriver setupHttpDriver() throws IOException { public void testConfirmInstanceInvalidAttestationData() { InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); InstanceConfirmation confirmation = new InstanceConfirmation(); confirmation.setAttestationData("invalid-json"); @@ -305,6 +349,7 @@ public void testConfirmInstanceInvalidAttestationData() { public void testConfirmInstanceAzureSubscriptionIssues() throws IOException { InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); InstanceConfirmation confirmation = new InstanceConfirmation(); @@ -327,7 +372,7 @@ public void testConfirmInstanceAzureSubscriptionIssues() throws IOException { assertTrue(ex.getMessage().contains("Unable to extract Azure Subscription id")); } - // add the subscription but different than what's in the data object + // add the subscription but different from what's in the data object attributes.put(InstanceProvider.ZTS_INSTANCE_AZURE_SUBSCRIPTION, "1111-3333"); @@ -345,6 +390,7 @@ public void testConfirmInstanceAzureSubscriptionIssues() throws IOException { public void testConfirmInstanceSanDnsMismatch() throws IOException { InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); InstanceConfirmation confirmation = new InstanceConfirmation(); @@ -383,6 +429,7 @@ public void testConfirmInstanceInvalidAccessToken() throws IOException { System.setProperty(InstanceAzureProvider.AZURE_PROP_ZTS_RESOURCE_URI, "https://azure-zts"); System.setProperty(InstanceAzureProvider.AZURE_PROP_OPENID_CONFIG_URI, "file://" + configFile.getCanonicalPath()); InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); InstanceConfirmation confirmation = new InstanceConfirmation(); @@ -429,6 +476,7 @@ public void testConfirmInstanceAudienceMismatch() throws IOException { System.setProperty(InstanceAzureProvider.AZURE_PROP_ZTS_RESOURCE_URI, "https://azure-zts-nomatch"); System.setProperty(InstanceAzureProvider.AZURE_PROP_OPENID_CONFIG_URI, "file://" + configFile.getCanonicalPath()); InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); InstanceConfirmation confirmation = new InstanceConfirmation(); @@ -478,6 +526,7 @@ public void testConfirmInstanceUnableToFetchVMDetails() throws IOException { System.setProperty(InstanceAzureProvider.AZURE_PROP_ZTS_RESOURCE_URI, "https://azure-zts"); System.setProperty(InstanceAzureProvider.AZURE_PROP_OPENID_CONFIG_URI, "file://" + configFile.getCanonicalPath()); InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); InstanceConfirmation confirmation = new InstanceConfirmation(); @@ -502,7 +551,6 @@ public void testConfirmInstanceUnableToFetchVMDetails() throws IOException { PublicKey publicKey = Crypto.loadPublicKey(ecPublicKey); provider.signingKeyResolver.addPublicKey("eckey1", publicKey); - provider.accessToken = "Bearer access-token"; // first with null http-driver @@ -513,10 +561,25 @@ public void testConfirmInstanceUnableToFetchVMDetails() throws IOException { assertTrue(ex.getMessage().contains("Unable to verify instance identity credentials")); } - // then will null access tokens + // then without access token from the external credentials provider provider.httpDriver = Mockito.mock(HttpDriver.class); - provider.accessToken = null; + ExternalCredentialsProvider externalCredentialsProvider = Mockito.mock(ExternalCredentialsProvider.class); + ExternalCredentialsResponse externalCredentialsResponse = new ExternalCredentialsResponse(); + externalCredentialsResponse.setAttributes(new HashMap<>()); + Mockito.when(externalCredentialsProvider.getExternalCredentials(any(), any(), any())).thenReturn(externalCredentialsResponse); + provider.setExternalCredentialsProvider(externalCredentialsProvider); + confirmation.setAttributes(attributes); + + try { + provider.confirmInstance(confirmation); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("Unable to verify instance identity credentials")); + } + + // then with null-responses + setUpExternalCredentialsProvider(provider); confirmation.setAttributes(attributes); try { @@ -529,7 +592,6 @@ public void testConfirmInstanceUnableToFetchVMDetails() throws IOException { // then with mock throwing an exception Mockito.when(provider.httpDriver.doGet(any(), any())).thenThrow(new IllegalArgumentException("bad client")); - provider.accessToken = "Bearer access-token"; confirmation.setAttributes(attributes); try { @@ -556,6 +618,7 @@ public void testConfirmInstanceInvalidVMDetails() throws IOException { System.setProperty(InstanceAzureProvider.AZURE_PROP_ZTS_RESOURCE_URI, "https://azure-zts"); System.setProperty(InstanceAzureProvider.AZURE_PROP_OPENID_CONFIG_URI, "file://" + configFile.getCanonicalPath()); InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); InstanceConfirmation confirmation = new InstanceConfirmation(); @@ -580,7 +643,6 @@ public void testConfirmInstanceInvalidVMDetails() throws IOException { PublicKey publicKey = Crypto.loadPublicKey(ecPublicKey); provider.signingKeyResolver.addPublicKey("eckey1", publicKey); - provider.accessToken = "Bearer access-token"; HttpDriver httpDriver = Mockito.mock(HttpDriver.class); Mockito.when(httpDriver.doGet(any(), any())).thenReturn("invalid-vmdetails"); @@ -610,6 +672,7 @@ public void testConfirmInstanceSubjectMismatch() throws IOException { System.setProperty(InstanceAzureProvider.AZURE_PROP_ZTS_RESOURCE_URI, "https://azure-zts"); System.setProperty(InstanceAzureProvider.AZURE_PROP_OPENID_CONFIG_URI, "file://" + configFile.getCanonicalPath()); InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); InstanceConfirmation confirmation = new InstanceConfirmation(); @@ -634,7 +697,6 @@ public void testConfirmInstanceSubjectMismatch() throws IOException { PublicKey publicKey = Crypto.loadPublicKey(ecPublicKey); provider.signingKeyResolver.addPublicKey("eckey1", publicKey); - provider.accessToken = "Bearer access-token"; final String vmDetails = "{\n" + @@ -681,6 +743,7 @@ public void testConfirmInstanceServiceNameMismatch() throws IOException { System.setProperty(InstanceAzureProvider.AZURE_PROP_ZTS_RESOURCE_URI, "https://azure-zts"); System.setProperty(InstanceAzureProvider.AZURE_PROP_OPENID_CONFIG_URI, "file://" + configFile.getCanonicalPath()); InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); InstanceConfirmation confirmation = new InstanceConfirmation(); @@ -705,7 +768,6 @@ public void testConfirmInstanceServiceNameMismatch() throws IOException { PublicKey publicKey = Crypto.loadPublicKey(ecPublicKey); provider.signingKeyResolver.addPublicKey("eckey1", publicKey); - provider.accessToken = "Bearer access-token"; final String vmDetails = "{\n" + @@ -752,6 +814,7 @@ public void testConfirmInstanceVMIdMismatch() throws IOException { System.setProperty(InstanceAzureProvider.AZURE_PROP_ZTS_RESOURCE_URI, "https://azure-zts"); System.setProperty(InstanceAzureProvider.AZURE_PROP_OPENID_CONFIG_URI, "file://" + configFile.getCanonicalPath()); InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); InstanceConfirmation confirmation = new InstanceConfirmation(); @@ -776,7 +839,6 @@ public void testConfirmInstanceVMIdMismatch() throws IOException { PublicKey publicKey = Crypto.loadPublicKey(ecPublicKey); provider.signingKeyResolver.addPublicKey("eckey1", publicKey); - provider.accessToken = "Bearer access-token"; final String vmDetails = "{\n" + @@ -823,6 +885,7 @@ public void testConfirmInstanceProviderMismatch() throws IOException { System.setProperty(InstanceAzureProvider.AZURE_PROP_ZTS_RESOURCE_URI, "https://azure-zts"); System.setProperty(InstanceAzureProvider.AZURE_PROP_OPENID_CONFIG_URI, "file://" + configFile.getCanonicalPath()); InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); InstanceConfirmation confirmation = new InstanceConfirmation(); @@ -847,7 +910,6 @@ public void testConfirmInstanceProviderMismatch() throws IOException { PublicKey publicKey = Crypto.loadPublicKey(ecPublicKey); provider.signingKeyResolver.addPublicKey("eckey1", publicKey); - provider.accessToken = "Bearer access-token"; final String vmDetails = "{\n" + @@ -946,128 +1008,17 @@ public void testInitializeFailedHttpClient() throws IOException { SSLContext sslContext = Mockito.mock(SSLContext.class); InstanceAzureProvider provider = new InstanceAzureProvider(); + setUpExternalCredentialsProvider(provider); provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", sslContext, null); assertNull(provider.httpDriver); // without http driver we can't fetch our vm details - assertNull(provider.fetchVMDetails(null)); + assertNull(provider.fetchVMDetails(null, null)); provider.close(); System.clearProperty(InstanceAzureProvider.AZURE_PROP_OPENID_CONFIG_URI); } - @Test - public void testFetchAccessTokenDataHttpFailure() throws IOException { - - InstanceAzureProvider provider = new InstanceAzureProvider(); - provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); - - HttpDriver httpDriver = Mockito.mock(HttpDriver.class); - Mockito.when(httpDriver.doGet(any(), any())).thenReturn(null); - provider.httpDriver = httpDriver; - - try { - provider.fetchAccessToken(); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("Unable to fetch access token")); - } - provider.close(); - } - - @Test - public void testFetchAccessTokenParseFailure() throws IOException { - - InstanceAzureProvider provider = new InstanceAzureProvider(); - provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); - - final String tokenDetails = "{\"token\":\"parse-failure"; - HttpDriver httpDriver = Mockito.mock(HttpDriver.class); - Mockito.when(httpDriver.doGet(any(), any())).thenReturn(tokenDetails); - provider.httpDriver = httpDriver; - - try { - provider.fetchAccessToken(); - fail(); - } catch (Exception ex) { - assertTrue(ex.getMessage().contains("Unable to parse access token response")); - } - provider.close(); - } - - @Test - public void testFetchAccessTokenDataNoToken() throws IOException { - - InstanceAzureProvider provider = new InstanceAzureProvider(); - provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); - - final String tokenDetails = "{\"token\":\"no-access-token\"}"; - HttpDriver httpDriver = Mockito.mock(HttpDriver.class); - Mockito.when(httpDriver.doGet(any(), any())).thenReturn(tokenDetails); - provider.httpDriver = httpDriver; - - try { - provider.fetchAccessToken(); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("Empty access token returned")); - } - provider.close(); - } - - @Test - public void testFetchAccessToken() throws IOException { - - InstanceAzureProvider provider = new InstanceAzureProvider(); - provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); - - final String tokenDetails = "{\"access_token\":\"access-token\"}"; - HttpDriver httpDriver = Mockito.mock(HttpDriver.class); - Mockito.when(httpDriver.doGet(any(), any())).thenReturn(tokenDetails); - provider.httpDriver = httpDriver; - - provider.fetchAccessToken(); - assertEquals(provider.accessToken, "Bearer access-token"); - provider.close(); - } - - @Test - public void testAzureCredentialsUpdater() throws IOException { - - InstanceAzureProvider provider = new InstanceAzureProvider(); - provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); - - final String tokenDetails = "{\"access_token\":\"access-token\"}"; - HttpDriver httpDriver = Mockito.mock(HttpDriver.class); - Mockito.when(httpDriver.doGet(any(), any())).thenReturn(tokenDetails); - provider.httpDriver = httpDriver; - - InstanceAzureProvider.AzureCredentialsUpdater updater = provider.new AzureCredentialsUpdater(); - updater.run(); - - assertEquals(provider.accessToken, "Bearer access-token"); - provider.close(); - } - - @Test - public void testAzureCredentialsUpdaterFailure() throws IOException { - - InstanceAzureProvider provider = new InstanceAzureProvider(); - provider.initialize("provider", "com.yahoo.athenz.instance.provider.impl.InstanceAzureProvider", null, null); - - final String tokenDetails = "{\"token\":\"no-access-token\"}"; - HttpDriver httpDriver = Mockito.mock(HttpDriver.class); - Mockito.when(httpDriver.doGet(any(), any())).thenReturn(tokenDetails); - provider.httpDriver = httpDriver; - - // there should be no exceptions - - InstanceAzureProvider.AzureCredentialsUpdater updater = provider.new AzureCredentialsUpdater(); - updater.run(); - - assertNull(provider.accessToken); - provider.close(); - } } diff --git a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/external/ExternalCredentialsProvider.java b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/external/ExternalCredentialsProvider.java index fe0a9a2908f..c443f8fe7ac 100644 --- a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/external/ExternalCredentialsProvider.java +++ b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/external/ExternalCredentialsProvider.java @@ -18,11 +18,14 @@ import com.yahoo.athenz.auth.Authorizer; import com.yahoo.athenz.auth.Principal; +import com.yahoo.athenz.auth.token.IdToken; import com.yahoo.athenz.common.server.rest.ResourceException; import com.yahoo.athenz.zts.DomainDetails; import com.yahoo.athenz.zts.ExternalCredentialsRequest; import com.yahoo.athenz.zts.ExternalCredentialsResponse; +import java.util.List; + public interface ExternalCredentialsProvider { /** @@ -38,11 +41,13 @@ public interface ExternalCredentialsProvider { * request object * @param principal principal object requesting credentials * @param domainDetails domain attributes including associated cloud provider account/project - * @param idToken principal's id token + * @param idTokenGroups groups extracted from the id token request + * @param idToken principal's id token, with basic fields filled out + * @param signer id token signer for the completed token * @param externalCredentialsRequest credentials request object * @return response object including the requested credentials * @throws ResourceException in case of any errors */ - ExternalCredentialsResponse getCredentials(Principal principal, DomainDetails domainDetails, - final String idToken, ExternalCredentialsRequest externalCredentialsRequest) throws ResourceException; + ExternalCredentialsResponse getCredentials(Principal principal, DomainDetails domainDetails, List idTokenGroups, + IdToken idToken, IdTokenSigner signer, ExternalCredentialsRequest externalCredentialsRequest) throws ResourceException; } diff --git a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/external/IdTokenSigner.java b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/external/IdTokenSigner.java new file mode 100644 index 00000000000..ca5c9593e70 --- /dev/null +++ b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/external/IdTokenSigner.java @@ -0,0 +1,30 @@ +/* + * Copyright The Athenz Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yahoo.athenz.common.server.external; + +import com.yahoo.athenz.auth.token.IdToken; + +public interface IdTokenSigner { + + /** + * Sign the given id token and return the signed token + * @param idToken id token to be signed + * @param keyType key type to be used for signing: rsa or ec + * @return signed token + */ + String sign(IdToken idToken, String keyType); + +} diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java index a16c4697051..1c0f364fdf4 100644 --- a/servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java @@ -4199,6 +4199,8 @@ void executePutDomainMeta(ResourceContext ctx, Domain domain, DomainMeta meta, .setApplicationId(domain.getApplicationId()) .setAccount(domain.getAccount()) .setAzureSubscription(domain.getAzureSubscription()) + .setAzureTenant(domain.getAzureTenant()) + .setAzureClient(domain.getAzureClient()) .setGcpProject(domain.getGcpProject()) .setGcpProjectNumber(domain.getGcpProjectNumber()) .setYpmId(domain.getYpmId()) @@ -4632,6 +4634,8 @@ void updateSystemMetaFields(Domain domain, final String attribute, boolean delet throw ZMSUtils.forbiddenError("unauthorized to reset system meta attribute: " + attribute, caller); } domain.setAzureSubscription(meta.getAzureSubscription()); + domain.setAzureTenant(meta.getAzureTenant()); + domain.setAzureClient(meta.getAzureClient()); break; case ZMSConsts.SYSTEM_META_GCP_PROJECT: if (!isDeleteSystemMetaAllowed(deleteAllowed, domain.getGcpProject(), meta.getGcpProject())) { diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSConsts.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSConsts.java index d868ae09346..8abf2ce621c 100644 --- a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSConsts.java +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSConsts.java @@ -271,6 +271,8 @@ public final class ZMSConsts { public static final String DB_COLUMN_AS_PRINCIPAL_NAME = "principal_name"; public static final String DB_COLUMN_SYSTEM_DISABLED = "system_disabled"; public static final String DB_COLUMN_AZURE_SUBSCRIPTION = "azure_subscription"; + public static final String DB_COLUMN_AZURE_TENANT = "azure_tenant"; + public static final String DB_COLUMN_AZURE_CLIENT = "azure_client"; public static final String DB_COLUMN_GCP_PROJECT_ID = "gcp_project"; public static final String DB_COLUMN_GCP_PROJECT_NUMBER = "gcp_project_number"; public static final String DB_COLUMN_BUSINESS_SERVICE = "business_service"; diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java index a0be8c659ec..02619596dc8 100644 --- a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java @@ -1600,6 +1600,8 @@ public Domain postTopLevelDomain(ResourceContext ctx, String auditRef, String re .setId(UUID.fromCurrentTime()) .setAccount(detail.getAccount()) .setAzureSubscription(detail.getAzureSubscription()) + .setAzureTenant(detail.getAzureTenant()) + .setAzureClient(detail.getAzureClient()) .setGcpProject(detail.getGcpProject()) .setGcpProjectNumber(detail.getGcpProjectNumber()) .setYpmId(productId) @@ -2389,6 +2391,8 @@ void validateDomainValues(Domain domain) { validateString(domain.getApplicationId(), TYPE_COMPOUND_NAME, caller); validateString(domain.getAccount(), TYPE_COMPOUND_NAME, caller); validateString(domain.getAzureSubscription(), TYPE_COMPOUND_NAME, caller); + validateString(domain.getAzureTenant(), TYPE_COMPOUND_NAME, caller); + validateString(domain.getAzureClient(), TYPE_COMPOUND_NAME, caller); validateString(domain.getGcpProject(), TYPE_COMPOUND_NAME, caller); validateString(domain.getGcpProjectNumber(), TYPE_COMPOUND_NAME, caller); validateString(domain.getUserAuthorityFilter(), TYPE_AUTHORITY_KEYWORDS, caller); @@ -2405,11 +2409,15 @@ void validateDomainValues(Domain domain) { if (!domainMetaStore.isValidAzureSubscription(domain.getName(), domain.getAzureSubscription())) { throw ZMSUtils.requestError("invalid azure subscription for domain", caller); } + // validate that azure details are specified correctly + if (!validateAllEmptyOrPresent(domain.getAzureSubscription(), domain.getAzureTenant(), domain.getAzureClient())) { + throw ZMSUtils.requestError("invalid azure details for domain, both subscription, tenant and client must be specified", caller); + } if (!domainMetaStore.isValidGcpProject(domain.getName(), domain.getGcpProject())) { throw ZMSUtils.requestError("invalid gcp project for domain", caller); } // validate that gcp project details are specified correctly - if (!validateGcpProjectDetails(domain.getGcpProject(), domain.getGcpProjectNumber())) { + if (!validateAllEmptyOrPresent(domain.getGcpProject(), domain.getGcpProjectNumber())) { throw ZMSUtils.requestError("invalid gcp project details for domain, both id and number must be specified", caller); } if (!domainMetaStore.isValidProductId(domain.getName(), domain.getYpmId())) { @@ -2430,12 +2438,16 @@ void validateDomainValues(Domain domain) { } } - boolean validateGcpProjectDetails(final String gcpProjectId, final String gcpProjectNumber) { - // for gcp project we must have both project id and project number specified, - // so we're going to check to make sure both are empty or have values - boolean gcpIdEmpty = StringUtil.isEmpty(gcpProjectId); - boolean gcpNumberEmpty = StringUtil.isEmpty(gcpProjectNumber); - return (gcpIdEmpty && gcpNumberEmpty) || (!gcpIdEmpty && !gcpNumberEmpty); + boolean validateAllEmptyOrPresent(final String... strings) { + if (strings == null) { + return true; + } + for (String s : strings) { + if (StringUtil.isEmpty(strings[0]) != StringUtil.isEmpty(s)) { + return false; + } + } + return true; } BitSet validateDomainSystemMetaStoreValues(Domain domain, DomainMeta meta, final String attributeName) { @@ -2456,7 +2468,12 @@ BitSet validateDomainSystemMetaStoreValues(Domain domain, DomainMeta meta, final } break; case ZMSConsts.SYSTEM_META_AZURE_SUBSCRIPTION: - if (ZMSUtils.metaValueChanged(domain.getAzureSubscription(), meta.getAzureSubscription())) { + if (!validateAllEmptyOrPresent(meta.getAzureSubscription(), meta.getAzureTenant(), meta.getAzureClient())) { + throw ZMSUtils.requestError("invalid azure details for domain, all of subscription, tenant and client must be specified", caller); + } + if (ZMSUtils.metaValueChanged(domain.getAzureSubscription(), meta.getAzureSubscription()) || + ZMSUtils.metaValueChanged(domain.getAzureTenant(), meta.getAzureTenant()) || + ZMSUtils.metaValueChanged(domain.getAzureClient(), meta.getAzureClient())) { if (!domainMetaStore.isValidAzureSubscription(domain.getName(), meta.getAzureSubscription())) { throw ZMSUtils.requestError("invalid azure subscription for domain", caller); } @@ -2464,7 +2481,7 @@ BitSet validateDomainSystemMetaStoreValues(Domain domain, DomainMeta meta, final } break; case ZMSConsts.SYSTEM_META_GCP_PROJECT: - if (!validateGcpProjectDetails(meta.getGcpProject(), meta.getGcpProjectNumber())) { + if (!validateAllEmptyOrPresent(meta.getGcpProject(), meta.getGcpProjectNumber())) { throw ZMSUtils.requestError("invalid gcp project details for domain, both id and number must be specified", caller); } if (ZMSUtils.metaValueChanged(domain.getGcpProject(), meta.getGcpProject()) || @@ -6853,6 +6870,8 @@ SignedDomain retrieveSignedDomainMeta(final Domain domain, final String metaAttr return null; } signedDomain.getDomain().setAzureSubscription(azureSubscription); + signedDomain.getDomain().setAzureTenant(domain.getAzureTenant()); + signedDomain.getDomain().setAzureClient(domain.getAzureClient()); break; case ZMSConsts.SYSTEM_META_GCP_PROJECT: final String gcpProject = domain.getGcpProject(); @@ -6895,6 +6914,8 @@ void setDomainDataAttributes(DomainData domainData, Domain domain) { domainData.setDescription(domain.getDescription()); domainData.setAccount(domain.getAccount()); domainData.setAzureSubscription(domain.getAzureSubscription()); + domainData.setAzureTenant(domain.getAzureTenant()); + domainData.setAzureClient(domain.getAzureClient()); domainData.setGcpProject(domain.getGcpProject()); domainData.setGcpProjectNumber(domain.getGcpProjectNumber()); domainData.setYpmId(domain.getYpmId()); @@ -6970,6 +6991,8 @@ SignedDomain retrieveSignedDomainData(Domain domain, boolean masterCopy, boolean } domainData.setAccount(athenzDomain.getDomain().getAccount()); domainData.setAzureSubscription(athenzDomain.getDomain().getAzureSubscription()); + domainData.setAzureTenant(athenzDomain.getDomain().getAzureTenant()); + domainData.setAzureClient(athenzDomain.getDomain().getAzureClient()); domainData.setYpmId(athenzDomain.getDomain().getYpmId()); domainData.setProductId(athenzDomain.getDomain().getProductId()); domainData.setApplicationId(athenzDomain.getDomain().getApplicationId()); diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnection.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnection.java index 836183bfe55..ae7beb4794c 100644 --- a/servers/zms/src/main/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnection.java +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnection.java @@ -80,14 +80,16 @@ public class JDBCConnection implements ObjectStoreConnection { + "(name, description, org, uuid, enabled, audit_enabled, account, ypm_id, application_id, cert_dns_domain," + " member_expiry_days, token_expiry_mins, service_cert_expiry_mins, role_cert_expiry_mins, sign_algorithm," + " service_expiry_days, user_authority_filter, group_expiry_days, azure_subscription, business_service," - + " member_purge_expiry_days, gcp_project, gcp_project_number, product_id, feature_flags, environment)" - + " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + + " member_purge_expiry_days, gcp_project, gcp_project_number, product_id, feature_flags, environment," + + " azure_tenant, azure_client)" + + " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; private static final String SQL_UPDATE_DOMAIN = "UPDATE domain " + "SET description=?, org=?, uuid=?, enabled=?, audit_enabled=?, account=?, ypm_id=?, application_id=?," + " cert_dns_domain=?, member_expiry_days=?, token_expiry_mins=?, service_cert_expiry_mins=?," + " role_cert_expiry_mins=?, sign_algorithm=?, service_expiry_days=?, user_authority_filter=?," + " group_expiry_days=?, azure_subscription=?, business_service=?, member_purge_expiry_days=?," - + " gcp_project=?, gcp_project_number=?, product_id=?, feature_flags=?, environment=? WHERE name=?;"; + + " gcp_project=?, gcp_project_number=?, product_id=?, feature_flags=?, environment=?," + + " azure_tenant=?, azure_client=? WHERE name=?;"; private static final String SQL_UPDATE_DOMAIN_MOD_TIMESTAMP = "UPDATE domain " + "SET modified=CURRENT_TIMESTAMP(3) WHERE name=?;"; private static final String SQL_GET_DOMAIN_MOD_TIMESTAMP = "SELECT modified FROM domain WHERE name=?;"; @@ -865,6 +867,8 @@ Domain saveDomainSettings(String domainName, ResultSet rs, boolean fetchAddlDeta .setId(saveUuidValue(rs.getString(ZMSConsts.DB_COLUMN_UUID))) .setAccount(saveValue(rs.getString(ZMSConsts.DB_COLUMN_ACCOUNT))) .setAzureSubscription(saveValue(rs.getString(ZMSConsts.DB_COLUMN_AZURE_SUBSCRIPTION))) + .setAzureTenant(saveValue(rs.getString(ZMSConsts.DB_COLUMN_AZURE_TENANT))) + .setAzureClient(saveValue(rs.getString(ZMSConsts.DB_COLUMN_AZURE_CLIENT))) .setGcpProject(saveValue(rs.getString(ZMSConsts.DB_COLUMN_GCP_PROJECT_ID))) .setGcpProjectNumber(saveValue(rs.getString(ZMSConsts.DB_COLUMN_GCP_PROJECT_NUMBER))) .setYpmId(rs.getInt(ZMSConsts.DB_COLUMN_YPM_ID)) @@ -953,6 +957,8 @@ public boolean insertDomain(Domain domain) { ps.setString(24, processInsertValue(domain.getProductId())); ps.setInt(25, processInsertValue(domain.getFeatureFlags())); ps.setString(26, processInsertValue(domain.getEnvironment())); + ps.setString(27, processInsertValue(domain.getAzureTenant())); + ps.setString(28, processInsertValue(domain.getAzureClient())); affectedRows = executeUpdate(ps, caller); } catch (SQLException ex) { throw sqlError(ex, caller); @@ -1097,7 +1103,9 @@ public boolean updateDomain(Domain domain) { ps.setString(23, processInsertValue(domain.getProductId())); ps.setInt(24, processInsertValue(domain.getFeatureFlags())); ps.setString(25, processInsertValue(domain.getEnvironment())); - ps.setString(26, domain.getName()); + ps.setString(26, processInsertValue(domain.getAzureTenant())); + ps.setString(27, processInsertValue(domain.getAzureClient())); + ps.setString(28, domain.getName()); affectedRows = executeUpdate(ps, caller); } catch (SQLException ex) { throw sqlError(ex, caller); diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/DBServiceTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/DBServiceTest.java index 04d7170dea8..f50b43c1ef0 100644 --- a/servers/zms/src/test/java/com/yahoo/athenz/zms/DBServiceTest.java +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/DBServiceTest.java @@ -3951,7 +3951,7 @@ public void testLookupDomainByAzureSubscription() { TopLevelDomain dom1 = createTopLevelDomainObject(domainName, "Test Domain1", "testOrg", adminUser); - dom1.setAzureSubscription("azure"); + dom1.setAzureSubscription("azure").setAzureTenant("tenant").setAzureClient("client"); zms.postTopLevelDomain(mockDomRsrcCtx, auditRef, null, dom1); DomainList list = zms.dbService.lookupDomainByAzureSubscription("azure"); @@ -5182,6 +5182,8 @@ public void testUpdateSystemMetaFields() { .setYpmId(1234) .setCertDnsDomain("athenz.cloud") .setAzureSubscription("azure") + .setAzureTenant("tenant") + .setAzureClient("client") .setBusinessService("123:business service") .setGcpProject("gcp") .setGcpProjectNumber("1234") @@ -5196,8 +5198,11 @@ public void testUpdateSystemMetaFields() { assertEquals(domain.getCertDnsDomain(), "athenz.cloud"); zms.dbService.updateSystemMetaFields(domain, "azuresubscription", true, meta); assertEquals(domain.getAzureSubscription(), "azure"); + assertEquals(domain.getAzureTenant(), "tenant"); + assertEquals(domain.getAzureClient(), "client"); zms.dbService.updateSystemMetaFields(domain, "gcpproject", true, meta); assertEquals(domain.getGcpProject(), "gcp"); + assertEquals(domain.getGcpProjectNumber(), "1234"); zms.dbService.updateSystemMetaFields(domain, "businessservice", true, meta); assertEquals(domain.getBusinessService(), "123:business service"); zms.dbService.updateSystemMetaFields(domain, "featureflags", true, meta); @@ -5217,6 +5222,8 @@ public void testUpdateSystemMetaFields() { .setYpmId(1234) .setCertDnsDomain("athenz.cloud") .setAzureSubscription("azure") + .setAzureTenant("tenant") + .setAzureClient("client") .setBusinessService("123:business service") .setGcpProject("gcp") .setGcpProjectNumber("1235") @@ -5229,8 +5236,11 @@ public void testUpdateSystemMetaFields() { assertEquals(domain1.getCertDnsDomain(), "athenz.cloud"); zms.dbService.updateSystemMetaFields(domain1, "azuresubscription", false, meta1); assertEquals(domain1.getAzureSubscription(), "azure"); + assertEquals(domain1.getAzureTenant(), "tenant"); + assertEquals(domain1.getAzureClient(), "client"); zms.dbService.updateSystemMetaFields(domain1, "gcpproject", false, meta1); assertEquals(domain1.getGcpProject(), "gcp"); + assertEquals(domain1.getGcpProjectNumber(), "1235"); zms.dbService.updateSystemMetaFields(domain1, "businessservice", false, meta1); assertEquals(domain1.getBusinessService(), "123:business service"); zms.dbService.updateSystemMetaFields(domain, "featureflags", false, meta); @@ -5243,6 +5253,8 @@ public void testUpdateSystemMetaFields() { .setYpmId(1234) .setCertDnsDomain("athenz.cloud") .setAzureSubscription("azure") + .setAzureTenant("tenant") + .setAzureClient("client") .setBusinessService("123:business service") .setGcpProject("gcp") .setGcpProjectNumber("1236") @@ -5252,6 +5264,8 @@ public void testUpdateSystemMetaFields() { .setYpmId(1235) .setCertDnsDomain("athenz.cloud.new") .setAzureSubscription("azure.new") + .setAzureTenant("tenant.new") + .setAzureClient("client.new") .setBusinessService("1234:business service2") .setGcpProject("gcp.new") .setGcpProjectNumber("1237") @@ -5316,6 +5330,8 @@ public void testUpdateSystemMetaFields() { .setYpmId(1234) .setCertDnsDomain("athenz.cloud") .setAzureSubscription("azure") + .setAzureTenant("tenant") + .setAzureClient("client") .setBusinessService("123:business service") .setGcpProject("gcp") .setGcpProjectNumber("1237") @@ -5325,6 +5341,8 @@ public void testUpdateSystemMetaFields() { .setYpmId(1234) .setCertDnsDomain("athenz.cloud") .setAzureSubscription("azure") + .setAzureTenant("tenant") + .setAzureClient("client") .setBusinessService("123:business service") .setGcpProject("gcp") .setGcpProjectNumber("1237") diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSDeleteDomainTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSDeleteDomainTest.java index ccb39d2a689..ac3e7b3e49d 100644 --- a/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSDeleteDomainTest.java +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSDeleteDomainTest.java @@ -1085,6 +1085,8 @@ public void testDeleteWithMetaAttributes() { meta.setGcpProject("gcp-project"); meta.setGcpProjectNumber("1234"); meta.setAzureSubscription("azure-subscription"); + meta.setAzureTenant("azure-tenant"); + meta.setAzureClient("azure-client"); zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_ACCOUNT, auditRef, meta); zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_GCP_PROJECT, auditRef, meta); zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_AZURE_SUBSCRIPTION, auditRef, meta); @@ -1125,6 +1127,8 @@ public void testDeleteWithMetaAttributes() { } meta.setAzureSubscription(null); + meta.setAzureTenant(null); + meta.setAzureClient(null); zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_AZURE_SUBSCRIPTION, auditRef, meta); zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); zmsTestInitializer.cleanupPrincipalSystemMetaDelete(zmsImpl, "domain"); diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSImplTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSImplTest.java index 3fedfa251d7..4ff2c4d39f0 100644 --- a/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSImplTest.java +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSImplTest.java @@ -571,6 +571,8 @@ public void testGetDomainListByAzureSubscription() { TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); dom1.setAzureSubscription("azure1"); + dom1.setAzureTenant("tenant"); + dom1.setAzureClient("client"); zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); DomainList domList = zmsImpl.getDomainList(ctx, null, null, null, null, @@ -1734,12 +1736,16 @@ public void testPutDomainMetaDefaults() { assertNull(resDom3.getBusinessService()); meta.setAzureSubscription("azure"); + meta.setAzureTenant("tenant"); + meta.setAzureClient("client"); zmsImpl.putDomainSystemMeta(ctx, domainName, "azuresubscription", auditRef, meta); resDom3 = zmsImpl.getDomain(ctx, domainName); assertNotNull(resDom3); assertEquals(resDom3.getOrg(), "neworg"); assertEquals(resDom3.getAccount(), "aws"); assertEquals(resDom3.getAzureSubscription(), "azure"); + assertEquals(resDom3.getAzureTenant(), "tenant"); + assertEquals(resDom3.getAzureClient(), "client"); assertNull(resDom3.getGcpProject()); assertNull(resDom3.getGcpProjectNumber()); assertNull(resDom3.getBusinessService()); @@ -1752,6 +1758,8 @@ public void testPutDomainMetaDefaults() { assertEquals(resDom3.getOrg(), "neworg"); assertEquals(resDom3.getAccount(), "aws"); assertEquals(resDom3.getAzureSubscription(), "azure"); + assertEquals(resDom3.getAzureTenant(), "tenant"); + assertEquals(resDom3.getAzureClient(), "client"); assertEquals(resDom3.getGcpProject(), "gcp"); assertEquals(resDom3.getGcpProjectNumber(), "1239"); assertNull(resDom3.getBusinessService()); @@ -1763,6 +1771,8 @@ public void testPutDomainMetaDefaults() { assertEquals(resDom3.getOrg(), "neworg"); assertEquals(resDom3.getAccount(), "aws"); assertEquals(resDom3.getAzureSubscription(), "azure"); + assertEquals(resDom3.getAzureTenant(), "tenant"); + assertEquals(resDom3.getAzureClient(), "client"); assertEquals(resDom3.getGcpProject(), "gcp"); assertEquals(resDom3.getGcpProjectNumber(), "1239"); assertEquals(resDom3.getBusinessService(), "123:business service"); @@ -20272,7 +20282,8 @@ public void testRetrieveSignedDomainMeta() { ZMSImpl zmsImpl = zmsTestInitializer.zmsInit(); Domain domainMeta = new Domain().setName("dom1").setYpmId(123).setModified(Timestamp.fromCurrentTime()) - .setAccount("1234").setAuditEnabled(true).setOrg("org").setAzureSubscription("4567") + .setAccount("1234").setAuditEnabled(true).setOrg("org") + .setAzureSubscription("4567").setAzureTenant("321").setAzureClient("999") .setBusinessService("123:business service").setGcpProject("gcp").setGcpProjectNumber("1240") .setProductId("abcd-123"); SignedDomain domain = zmsImpl.retrieveSignedDomainMeta(domainMeta, null); @@ -20281,6 +20292,7 @@ public void testRetrieveSignedDomainMeta() { assertNull(domain.getDomain().getOrg()); assertNull(domain.getDomain().getAuditEnabled()); assertNull(domain.getDomain().getAzureSubscription()); + assertNull(domain.getDomain().getAzureTenant()); assertNull(domain.getDomain().getGcpProject()); assertNull(domain.getDomain().getGcpProjectNumber()); assertNull(domain.getDomain().getBusinessService()); @@ -20292,6 +20304,8 @@ public void testRetrieveSignedDomainMeta() { assertNull(domain.getDomain().getOrg()); assertNull(domain.getDomain().getAuditEnabled()); assertNull(domain.getDomain().getAzureSubscription()); + assertNull(domain.getDomain().getAzureTenant()); + assertNull(domain.getDomain().getAzureClient()); assertNull(domain.getDomain().getGcpProject()); assertNull(domain.getDomain().getGcpProjectNumber()); assertNull(domain.getDomain().getBusinessService()); @@ -20303,6 +20317,8 @@ public void testRetrieveSignedDomainMeta() { assertNull(domain.getDomain().getOrg()); assertNull(domain.getDomain().getAuditEnabled()); assertNull(domain.getDomain().getAzureSubscription()); + assertNull(domain.getDomain().getAzureTenant()); + assertNull(domain.getDomain().getAzureClient()); assertNull(domain.getDomain().getGcpProject()); assertNull(domain.getDomain().getGcpProjectNumber()); assertNull(domain.getDomain().getBusinessService()); @@ -20314,6 +20330,8 @@ public void testRetrieveSignedDomainMeta() { assertNull(domain.getDomain().getOrg()); assertNull(domain.getDomain().getAuditEnabled()); assertNull(domain.getDomain().getAzureSubscription()); + assertNull(domain.getDomain().getAzureTenant()); + assertNull(domain.getDomain().getAzureClient()); assertNull(domain.getDomain().getGcpProject()); assertNull(domain.getDomain().getGcpProjectNumber()); assertNull(domain.getDomain().getBusinessService()); @@ -20321,6 +20339,8 @@ public void testRetrieveSignedDomainMeta() { domain = zmsImpl.retrieveSignedDomainMeta(domainMeta, "azuresubscription"); assertEquals(domain.getDomain().getAzureSubscription(), "4567"); + assertEquals(domain.getDomain().getAzureTenant(), "321"); + assertEquals(domain.getDomain().getAzureClient(), "999"); assertNull(domain.getDomain().getAccount()); assertNull(domain.getDomain().getYpmId()); assertNull(domain.getDomain().getOrg()); @@ -20338,6 +20358,8 @@ public void testRetrieveSignedDomainMeta() { assertNull(domain.getDomain().getOrg()); assertNull(domain.getDomain().getAuditEnabled()); assertNull(domain.getDomain().getAzureSubscription()); + assertNull(domain.getDomain().getAzureTenant()); + assertNull(domain.getDomain().getAzureClient()); assertNull(domain.getDomain().getBusinessService()); assertNull(domain.getDomain().getProductId()); @@ -20348,6 +20370,8 @@ public void testRetrieveSignedDomainMeta() { assertNull(domain.getDomain().getOrg()); assertNull(domain.getDomain().getAuditEnabled()); assertNull(domain.getDomain().getAzureSubscription()); + assertNull(domain.getDomain().getAzureTenant()); + assertNull(domain.getDomain().getAzureClient()); assertNull(domain.getDomain().getGcpProject()); assertNull(domain.getDomain().getGcpProjectNumber()); assertNull(domain.getDomain().getProductId()); @@ -20359,6 +20383,8 @@ public void testRetrieveSignedDomainMeta() { assertNull(domain.getDomain().getOrg()); assertNull(domain.getDomain().getAuditEnabled()); assertNull(domain.getDomain().getAzureSubscription()); + assertNull(domain.getDomain().getAzureTenant()); + assertNull(domain.getDomain().getAzureClient()); assertNull(domain.getDomain().getGcpProject()); assertNull(domain.getDomain().getGcpProjectNumber()); assertNull(domain.getDomain().getBusinessService()); @@ -20366,6 +20392,8 @@ public void testRetrieveSignedDomainMeta() { domain = zmsImpl.retrieveSignedDomainMeta(domainMeta, "all"); assertEquals(domain.getDomain().getAccount(), "1234"); assertEquals(domain.getDomain().getAzureSubscription(), "4567"); + assertEquals(domain.getDomain().getAzureTenant(), "321"); + assertEquals(domain.getDomain().getAzureClient(), "999"); assertEquals(domain.getDomain().getGcpProject(), "gcp"); assertEquals(domain.getDomain().getGcpProjectNumber(), "1240"); assertEquals(domain.getDomain().getYpmId().intValue(), 123); @@ -20379,6 +20407,8 @@ public void testRetrieveSignedDomainMeta() { assertNull(domain); domainMeta.setAzureSubscription(null); + domainMeta.setAzureTenant(null); + domainMeta.setAzureClient(null); domain = zmsImpl.retrieveSignedDomainMeta(domainMeta, "azuresubscription"); assertNull(domain); @@ -21116,6 +21146,8 @@ public void testPostSubDomainSkipSystemAttributes() { TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser(), ctx.principal().getFullName()); dom1.setAzureSubscription("azure"); + dom1.setAzureTenant("tenant"); + dom1.setAzureClient("client"); dom1.setAccount("aws"); dom1.setGcpProject("gcp"); dom1.setGcpProjectNumber("1250"); @@ -21124,12 +21156,16 @@ public void testPostSubDomainSkipSystemAttributes() { Domain dom1Res = zmsImpl.getDomain(ctx, domainName); assertEquals(dom1Res.getAccount(), "aws"); assertEquals(dom1Res.getAzureSubscription(), "azure"); + assertEquals(dom1Res.getAzureTenant(), "tenant"); + assertEquals(dom1Res.getAzureClient(), "client"); assertEquals(dom1Res.getGcpProject(), "gcp"); assertEquals(dom1Res.getGcpProjectNumber(), "1250"); SubDomain dom2 = zmsTestInitializer.createSubDomainObject("sub", domainName, "Test Domain2", "testOrg", zmsTestInitializer.getAdminUser()); dom2.setAzureSubscription("azure"); + dom2.setAzureTenant("tenant"); + dom2.setAzureClient("client"); dom2.setAccount("aws"); dom2.setGcpProject("gcp"); dom2.setGcpProjectNumber("1251"); @@ -21140,6 +21176,8 @@ public void testPostSubDomainSkipSystemAttributes() { Domain dom2Res = zmsImpl.getDomain(ctx, domainName + ".sub"); assertNull(dom2Res.getAccount()); assertNull(dom2Res.getAzureSubscription()); + assertNull(dom2Res.getAzureTenant()); + assertNull(dom2Res.getAzureClient()); assertNull(dom2Res.getGcpProject()); assertNull(dom2Res.getGcpProjectNumber()); @@ -29160,14 +29198,14 @@ public void testAzureSubscriptionUniquenessCheck() { TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName1, "Test Domain1", "testOrg", "user.user1"); - dom1.setAzureSubscription("azure1"); + dom1.setAzureSubscription("azure1").setAzureTenant("az-tenant").setAzureClient("az-client"); zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); // create another domain with the same subscription which should be rejected TopLevelDomain dom2 = zmsTestInitializer.createTopLevelDomainObject(domainName2, "Test Domain1", "testOrg", "user.user1"); - dom2.setAzureSubscription("azure1"); + dom2.setAzureSubscription("azure1").setAzureTenant("az-tenant").setAzureClient("az-client"); try { zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom2); fail(); @@ -29476,6 +29514,8 @@ public void testPostDomainInvalidDomainMetaStoreValues() { try { dom1.setAccount("valid-aws-account"); dom1.setAzureSubscription("invalid-azure-subscription"); + dom1.setAzureTenant("tenant"); + dom1.setAzureClient("client"); zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); fail(); } catch (ResourceException ex) { @@ -29483,7 +29523,6 @@ public void testPostDomainInvalidDomainMetaStoreValues() { } try { - dom1.setAccount("valid-aws-account"); dom1.setAzureSubscription("valid-azure-subscription"); dom1.setGcpProject("invalid-gcp-project"); dom1.setGcpProjectNumber("1200"); @@ -29505,8 +29544,6 @@ public void testPostDomainInvalidDomainMetaStoreValues() { } try { - dom1.setGcpProject("valid-gcp-project"); - dom1.setGcpProjectNumber("1200"); dom1.setYpmId(101); dom1.setProductId("invalid-product-id"); zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); @@ -29515,12 +29552,32 @@ public void testPostDomainInvalidDomainMetaStoreValues() { assertTrue(ex.getMessage().contains("invalid product id")); } - // specify gcp project but no project number + // specify azure subscription but no tenant try { - dom1.setYpmId(101); dom1.setProductId("valid-product-id"); - dom1.setGcpProject("valid-gcp-project"); + dom1.setAzureTenant(null); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid azure details")); + } + + // specify azure tenant but no client + + try { + dom1.setAzureTenant("tenant"); + dom1.setAzureClient(null); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid azure details")); + } + + // specify gcp project but no project number + + try { + dom1.setAzureClient("client"); dom1.setGcpProjectNumber(null); zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); fail(); @@ -29528,8 +29585,6 @@ public void testPostDomainInvalidDomainMetaStoreValues() { assertTrue(ex.getMessage().contains("invalid gcp project")); } - dom1.setYpmId(101); - dom1.setGcpProject("valid-gcp-project"); dom1.setGcpProjectNumber("1200"); zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); @@ -29538,7 +29593,10 @@ public void testPostDomainInvalidDomainMetaStoreValues() { assertEquals(domain.getBusinessService(), "valid-business-service"); assertEquals(domain.getAccount(), "valid-aws-account"); assertEquals(domain.getAzureSubscription(), "valid-azure-subscription"); + assertEquals(domain.getAzureTenant(), "tenant"); + assertEquals(domain.getAzureClient(), "client"); assertEquals(domain.getGcpProject(), "valid-gcp-project"); + assertEquals(domain.getGcpProjectNumber(), "1200"); assertEquals(domain.getYpmId().intValue(), 101); zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); @@ -29620,22 +29678,70 @@ public void testPutDomainSystemMetaInvalidDomainMetaStoreValues() { zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_ACCOUNT, auditRef, meta); - // next azure subscription + // next invalid azure subscription try { meta.setAzureSubscription("invalid-azure-subscription"); zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_AZURE_SUBSCRIPTION, auditRef, meta); fail(); } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid azure subscription")); + assertTrue(ex.getMessage().contains("invalid azure details")); + } + + // next azure subscription without azure tenant + + try { + meta.setAzureSubscription("valid-azure-subscription"); + meta.setAzureTenant(null); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_AZURE_SUBSCRIPTION, auditRef, meta); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid azure details")); } - meta.setAzureSubscription("valid-azure-subscription"); + // next azure subscription and tenant without client + + try { + meta.setAzureTenant("tenant"); + meta.setAzureClient(null); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_AZURE_SUBSCRIPTION, auditRef, meta); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid azure details")); + } + + meta.setAzureClient("client"); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_AZURE_SUBSCRIPTION, auditRef, meta); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getAzureSubscription(), "valid-azure-subscription"); + assertEquals(domain.getAzureTenant(), "tenant"); + assertEquals(domain.getAzureClient(), "client"); + + // now keep the azure subscription but update the azure tenant + meta.setAzureTenant("tenant2"); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_AZURE_SUBSCRIPTION, auditRef, meta); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getAzureSubscription(), "valid-azure-subscription"); + assertEquals(domain.getAzureTenant(), "tenant2"); + assertEquals(domain.getAzureClient(), "client"); + + // second time no-op since nothing has changed + + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_AZURE_SUBSCRIPTION, auditRef, meta); + + // now keep the azure tenant but update the azure client + meta.setAzureClient("client2"); zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_AZURE_SUBSCRIPTION, auditRef, meta); domain = zmsImpl.getDomain(ctx, domainName); assertNotNull(domain); assertEquals(domain.getAzureSubscription(), "valid-azure-subscription"); + assertEquals(domain.getAzureTenant(), "tenant2"); + assertEquals(domain.getAzureClient(), "client2"); // second time no-op since nothing has changed @@ -31476,20 +31582,20 @@ public void testValidateGroupAuditEnabledFlag() { } @Test - public void testValidateGcpProjectDetails() { + public void testValidateAllEmptyOrPresent() { ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - assertTrue(zmsImpl.validateGcpProjectDetails("", "")); - assertTrue(zmsImpl.validateGcpProjectDetails(null, "")); - assertTrue(zmsImpl.validateGcpProjectDetails("", null)); - assertTrue(zmsImpl.validateGcpProjectDetails(null, null)); - assertTrue(zmsImpl.validateGcpProjectDetails("gcp", "123400")); + assertTrue(zmsImpl.validateAllEmptyOrPresent("", "")); + assertTrue(zmsImpl.validateAllEmptyOrPresent(null, "")); + assertTrue(zmsImpl.validateAllEmptyOrPresent("", null)); + assertTrue(zmsImpl.validateAllEmptyOrPresent(null, null)); + assertTrue(zmsImpl.validateAllEmptyOrPresent("gcp", "123400")); - assertFalse(zmsImpl.validateGcpProjectDetails("", "1234")); - assertFalse(zmsImpl.validateGcpProjectDetails(null, "1234")); - assertFalse(zmsImpl.validateGcpProjectDetails("1234", "")); - assertFalse(zmsImpl.validateGcpProjectDetails("1234", null)); + assertFalse(zmsImpl.validateAllEmptyOrPresent("", "1234")); + assertFalse(zmsImpl.validateAllEmptyOrPresent(null, "1234")); + assertFalse(zmsImpl.validateAllEmptyOrPresent("1234", "")); + assertFalse(zmsImpl.validateAllEmptyOrPresent("1234", null)); } @Test diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnectionTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnectionTest.java index c0dd73f7dc7..a4ab37d67a2 100644 --- a/servers/zms/src/test/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnectionTest.java +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnectionTest.java @@ -78,6 +78,8 @@ public void testGetDomain() throws Exception { Mockito.doReturn(1001).when(mockResultSet).getInt(ZMSConsts.DB_COLUMN_YPM_ID); Mockito.doReturn(90).when(mockResultSet).getInt(ZMSConsts.DB_COLUMN_MEMBER_PURGE_EXPIRY_DAYS); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_AZURE_SUBSCRIPTION); + Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_AZURE_TENANT); + Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_AZURE_CLIENT); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_GCP_PROJECT_ID); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_GCP_PROJECT_NUMBER); Mockito.doReturn("service1").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_BUSINESS_SERVICE); @@ -124,6 +126,8 @@ public void testGetDomainWithAuditEnabled() throws Exception { Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_SIGN_ALGORITHM); Mockito.doReturn("OnShore").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_FILTER); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_AZURE_SUBSCRIPTION); + Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_AZURE_TENANT); + Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_AZURE_CLIENT); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_GCP_PROJECT_ID); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_GCP_PROJECT_NUMBER); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_BUSINESS_SERVICE); @@ -410,6 +414,8 @@ public void testGetDomainAllFields() throws Exception { Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_SIGN_ALGORITHM); Mockito.doReturn("OnShore").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_FILTER); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_AZURE_SUBSCRIPTION); + Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_AZURE_TENANT); + Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_AZURE_CLIENT); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_GCP_PROJECT_ID); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_GCP_PROJECT_NUMBER); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_BUSINESS_SERVICE); @@ -494,6 +500,8 @@ public void testInsertDomainWithAccountInfo() throws Exception { .setAccount("123456789") .setYpmId(1011) .setAzureSubscription("1234") + .setAzureTenant("321") + .setAzureClient("999") .setGcpProject("gcp") .setGcpProjectNumber("1234") .setProductId("abcd-1234") @@ -517,6 +525,8 @@ public void testInsertDomainWithAccountInfo() throws Exception { Mockito.verify(mockPrepStmt, times(1)).setString(23, "1234"); Mockito.verify(mockPrepStmt, times(1)).setString(24, "abcd-1234"); Mockito.verify(mockPrepStmt, times(1)).setInt(25, 3); + Mockito.verify(mockPrepStmt, times(1)).setString(27, "321"); + Mockito.verify(mockPrepStmt, times(1)).setString(28, "999"); jdbcConn.close(); } @@ -644,6 +654,8 @@ public void testUpdateDomain() throws Exception { .setSignAlgorithm("ec") .setUserAuthorityFilter("OnShore") .setAzureSubscription("azure") + .setAzureTenant("tenant") + .setAzureClient("client") .setBusinessService("service1") .setMemberPurgeExpiryDays(90) .setGcpProject("gcp") @@ -681,7 +693,9 @@ public void testUpdateDomain() throws Exception { Mockito.verify(mockPrepStmt, times(1)).setString(23, "abcd-1234"); Mockito.verify(mockPrepStmt, times(1)).setInt(24, 3); Mockito.verify(mockPrepStmt, times(1)).setString(25, "production"); - Mockito.verify(mockPrepStmt, times(1)).setString(26, "my-domain"); + Mockito.verify(mockPrepStmt, times(1)).setString(26, "tenant"); + Mockito.verify(mockPrepStmt, times(1)).setString(27, "client"); + Mockito.verify(mockPrepStmt, times(1)).setString(28, "my-domain"); jdbcConn.close(); } @@ -723,7 +737,9 @@ public void testUpdateDomainNullFields() throws Exception { Mockito.verify(mockPrepStmt, times(1)).setString(23, ""); Mockito.verify(mockPrepStmt, times(1)).setInt(24, 0); Mockito.verify(mockPrepStmt, times(1)).setString(25, ""); - Mockito.verify(mockPrepStmt, times(1)).setString(26, "my-domain"); + Mockito.verify(mockPrepStmt, times(1)).setString(26, ""); + Mockito.verify(mockPrepStmt, times(1)).setString(27, ""); + Mockito.verify(mockPrepStmt, times(1)).setString(28, "my-domain"); jdbcConn.close(); } @@ -6446,6 +6462,8 @@ public void testListModifiedDomains() throws Exception { Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_NOTIFY_ROLES)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_FILTER)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_AZURE_SUBSCRIPTION)).thenReturn(""); + Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_AZURE_TENANT)).thenReturn(""); + Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_AZURE_CLIENT)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_GCP_PROJECT_ID)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_GCP_PROJECT_NUMBER)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_BUSINESS_SERVICE)).thenReturn(""); @@ -6603,6 +6621,8 @@ public void testGetAthenzDomain() throws Exception { Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_FILTER)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_USER_AUTHORITY_EXPIRATION)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_AZURE_SUBSCRIPTION)).thenReturn(""); + Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_AZURE_TENANT)).thenReturn(""); + Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_AZURE_CLIENT)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_GCP_PROJECT_ID)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_GCP_PROJECT_NUMBER)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_BUSINESS_SERVICE)).thenReturn(""); diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/ExternalCredentialsManager.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/ExternalCredentialsManager.java index ad0a79c5715..317e359d143 100644 --- a/servers/zts/src/main/java/com/yahoo/athenz/zts/ExternalCredentialsManager.java +++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/ExternalCredentialsManager.java @@ -17,6 +17,7 @@ import com.yahoo.athenz.auth.Authorizer; import com.yahoo.athenz.common.server.external.ExternalCredentialsProvider; +import com.yahoo.athenz.zts.external.azure.AzureAccessTokenProvider; import com.yahoo.athenz.zts.external.gcp.GcpAccessTokenProvider; import java.util.*; @@ -28,10 +29,15 @@ public class ExternalCredentialsManager { public ExternalCredentialsManager(Authorizer authorizer) { externalCredentialsProviders = new HashMap<>(); + ExternalCredentialsProvider gcpProvider = new GcpAccessTokenProvider(); gcpProvider.setAuthorizer(authorizer); externalCredentialsProviders.put(ZTSConsts.ZTS_EXTERNAL_CREDS_PROVIDER_GCP, gcpProvider); + ExternalCredentialsProvider azureProvider = new AzureAccessTokenProvider(); + azureProvider.setAuthorizer(authorizer); + externalCredentialsProviders.put(ZTSConsts.ZTS_EXTERNAL_CREDS_PROVIDER_AZURE, azureProvider); + // configure which providers are enabled final String providerList = System.getProperty(ZTSConsts.ZTS_PROP_EXTERNAL_CREDS_PROVIDERS, diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSConsts.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSConsts.java index b5d9e0f0c71..853311e45fd 100644 --- a/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSConsts.java +++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSConsts.java @@ -269,6 +269,7 @@ public final class ZTSConsts { public static final String ZTS_PROP_EXTERNAL_CREDS_PROVIDERS = "athenz.zts.external_creds_providers"; public static final String ZTS_EXTERNAL_CREDS_PROVIDER_GCP = "gcp"; public static final String ZTS_EXTERNAL_CREDS_PROVIDER_AWS = "aws"; + public static final String ZTS_EXTERNAL_CREDS_PROVIDER_AZURE = "azure"; public static final String ZTS_EXTERNAL_ATTR_ROLE_NAME = "athenzRoleName"; public static final String ZTS_EXTERNAL_ATTR_SCOPE = "athenzScope"; diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSImpl.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSImpl.java index ecda6ac3cca..0cf9400a9cf 100644 --- a/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSImpl.java +++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSImpl.java @@ -37,6 +37,7 @@ import com.yahoo.athenz.common.server.dns.HostnameResolver; import com.yahoo.athenz.common.server.dns.HostnameResolverFactory; import com.yahoo.athenz.common.server.external.ExternalCredentialsProvider; +import com.yahoo.athenz.common.server.external.IdTokenSigner; import com.yahoo.athenz.common.server.log.AuditLogger; import com.yahoo.athenz.common.server.log.AuditLoggerFactory; import com.yahoo.athenz.common.server.notification.NotificationManager; @@ -4053,10 +4054,20 @@ InstanceConfirmation generateInstanceConfirmObject(ResourceContext ctx, final St if (awsAccount != null) { attributes.put(InstanceProvider.ZTS_INSTANCE_AWS_ACCOUNT, awsAccount); } + final String azureSubscription = cloudStore.getAzureSubscription(domain); if (azureSubscription != null) { attributes.put(InstanceProvider.ZTS_INSTANCE_AZURE_SUBSCRIPTION, azureSubscription); } + final String azureTenant = cloudStore.getAzureTenant(domain); + if (azureTenant != null) { + attributes.put(InstanceProvider.ZTS_INSTANCE_AZURE_TENANT, azureTenant); + } + final String azureClient = cloudStore.getAzureClient(domain); + if (azureClient != null) { + attributes.put(InstanceProvider.ZTS_INSTANCE_AZURE_CLIENT, azureClient); + } + final String gcpProject = cloudStore.getGCPProjectId(domain); if (gcpProject != null) { @@ -4978,28 +4989,31 @@ public ExternalCredentialsResponse postExternalCredentialsRequest(ResourceContex IdToken idToken = new IdToken(); idToken.setVersion(1); - idToken.setAudience(getIdTokenAudience(extCredsRequest.getClientId(), false, idTokenGroups)); - idToken.setSubject(principalName); final String issuerOption = extCredsAttributes.get(ZTSConsts.ZTS_EXTERNAL_ATTR_ISSUER_OPTION); idToken.setIssuer(isOidcPortRequest(ctx.request(), issuerOption) ? ztsOIDCPortIssuer : ztsOpenIDIssuer); idToken.setNonce(Crypto.randomSalt()); - idToken.setGroups(idTokenGroups); idToken.setIssueTime(iat); idToken.setAuthTime(iat); idToken.setExpiryTime(iat + idTokenMaxTimeout); - ServerPrivateKey signPrivateKey = getSignPrivateKey(null); - final String signedIdToken = idToken.getSignedToken(signPrivateKey.getKey(), signPrivateKey.getId(), - signPrivateKey.getAlgorithm()); + IdTokenSigner idTokenSigner = new IdTokenSigner() { + @Override public String sign(IdToken idToken, String keyType) { + ServerPrivateKey signPrivateKey = getSignPrivateKey(keyType); + return idToken.getSignedToken(signPrivateKey.getKey(), signPrivateKey.getId(), + signPrivateKey.getAlgorithm()); + } + }; DomainDetails domainDetails = new DomainDetails().setName(domainName) .setGcpProjectId(cloudStore.getGCPProjectId(domainName)) .setGcpProjectNumber(cloudStore.getGCPProjectNumber(domainName)) .setAwsAccount(cloudStore.getAwsAccount(domainName)) - .setAzureSubscription(cloudStore.getAzureSubscription(domainName)); - + .setAzureSubscription(cloudStore.getAzureSubscription(domainName)) + .setAzureTenant(cloudStore.getAzureTenant(domainName)) + .setAzureClient(cloudStore.getAzureClient(domainName)); try { - return externalCredentialsProvider.getCredentials(principal, domainDetails, signedIdToken, extCredsRequest); + return externalCredentialsProvider.getCredentials(principal, domainDetails, idTokenGroups, + idToken, idTokenSigner, extCredsRequest); } catch (com.yahoo.athenz.common.server.rest.ResourceException ex) { throw forbiddenError(ex.getMessage(), caller, domainName, principalDomain); } diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/external/azure/AzureAccessTokenProvider.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/external/azure/AzureAccessTokenProvider.java new file mode 100644 index 00000000000..dc012301291 --- /dev/null +++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/external/azure/AzureAccessTokenProvider.java @@ -0,0 +1,252 @@ +/* + * Copyright The Athenz Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yahoo.athenz.zts.external.azure; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.athenz.auth.Authorizer; +import com.yahoo.athenz.auth.Principal; +import com.yahoo.athenz.auth.token.IdToken; +import com.yahoo.athenz.common.server.external.ExternalCredentialsProvider; +import com.yahoo.athenz.common.server.external.IdTokenSigner; +import com.yahoo.athenz.common.server.http.HttpDriver; +import com.yahoo.athenz.common.server.http.HttpDriverResponse; +import com.yahoo.athenz.common.server.rest.ResourceException; +import com.yahoo.athenz.zts.AccessTokenResponse; +import com.yahoo.athenz.zts.DomainDetails; +import com.yahoo.athenz.zts.ExternalCredentialsRequest; +import com.yahoo.athenz.zts.ExternalCredentialsResponse; +import com.yahoo.rdl.Timestamp; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.message.BasicNameValuePair; +import org.eclipse.jetty.util.StringUtil; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class AzureAccessTokenProvider implements ExternalCredentialsProvider { + + static final String AZURE_MGMT_URL = "https://management.azure.com"; + static final String AZURE_MGMT_SCOPE = AZURE_MGMT_URL + "/.default"; + + static final String AZURE_OPENID_BASE_URI = "https://login.microsoftonline.com/"; + static final String AZURE_SCOPE_ACTION = "azure.scope_access"; + + static final String AZURE_CLIENT_ID = "azureClientId"; + static final String AZURE_RESOURCE_GROUP = "azureResourceGroup"; + static final String AZURE_CLIENT_NAME = "azureClientName"; + static final String AZURE_TOKEN_SCOPE = "azureTokenScope"; + static final String AZURE_ACCESS_TOKEN = "accessToken"; + static final String AZURE_TENANT = "azureTenant"; + static final String AZURE_SUBSCRIPTION = "azureSubscription"; + + static final String SYSTEM_AZURE_CLIENT_ROLE = "athenz.azure:role.azure-client"; + + final ObjectMapper jsonMapper; + final Map systemAccessTokenCache; + HttpDriver httpDriver; + Authorizer authorizer; + + public AzureAccessTokenProvider() { + jsonMapper = new ObjectMapper(); + systemAccessTokenCache = new ConcurrentHashMap<>(); + jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + httpDriver = new HttpDriver.Builder(null, null) + .clientConnectTimeoutMs(1000) + .clientReadTimeoutMs(3000) + .build(); + authorizer = null; + } + + /** Package-private for unit tests. */ + void setHttpDriver(HttpDriver httpDriver) { + this.httpDriver = httpDriver; + } + + @Override + public void setAuthorizer(Authorizer authorizer) { + this.authorizer = authorizer; + } + + private static String getRequestAttribute(Map attributes, String attrName, String attrDefaultValue) { + String value = attributes.get(attrName); + return StringUtil.isEmpty(value) ? attrDefaultValue : value; + } + + /** + * https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#third-case-access-token-request-with-a-federated-credential + */ + @Override + public ExternalCredentialsResponse getCredentials(Principal principal, DomainDetails domainDetails, List idTokenGroups, + IdToken idToken, IdTokenSigner idTokenSigner, ExternalCredentialsRequest externalCredentialsRequest) + throws ResourceException { + + // First make sure that our required components are available and configured + + if (authorizer == null) { + throw new ResourceException(ResourceException.FORBIDDEN, "ZTS authorizer not configured"); + } + + // Then verify the Azure tenant and client (for ZTS) is configured on the domain + String azureTenant = domainDetails.getAzureTenant(); + if (StringUtil.isEmpty(azureTenant)) { + throw new ResourceException(ResourceException.FORBIDDEN, "azure tenant not configured for domain"); + } + String azureProviderIdentityLoginUri = AZURE_OPENID_BASE_URI + azureTenant + "/oauth2/v2.0/token"; + + String systemAzureClient = domainDetails.getAzureClient(); + if (StringUtil.isEmpty(systemAzureClient)) { + throw new ResourceException(ResourceException.FORBIDDEN, "azure client not configured for domain"); + } + + // Verify a single role was requested, and accessible to the principal, and use this as the id token subject + + if (idTokenGroups.size() != 1) { + throw new ResourceException(ResourceException.BAD_REQUEST, "must specify exactly one accessible role"); + } + idToken.setAudience("api://AzureADTokenExchange"); + + // Check that the request contains an Azure client ID, or resource group and client names; or is a request for the system access token. + + Map attributes = externalCredentialsRequest.getAttributes(); + String requestAzureClientId = getRequestAttribute(attributes, AZURE_CLIENT_ID, null); + if (StringUtil.isEmpty(requestAzureClientId)) { + + // If the azure client ID is not indicated directly, we need the system access token, either for returning, or for finding the client ID. + + ExternalCredentialsResponse systemAccessToken = getSystemAccessToken(azureProviderIdentityLoginUri, domainDetails, idToken, idTokenSigner); + + // If the requested role is the system Azure client role, this is an internal request for its access token, so we return that. + + if (idTokenGroups.get(0).equals(SYSTEM_AZURE_CLIENT_ROLE)) { + return systemAccessToken; + } else { // Otherwise, we need to find the Azure client ID from the requested resource group and client name + String azureResourceGroup = getRequestAttribute(attributes, AZURE_RESOURCE_GROUP, null); + String azureClientName = getRequestAttribute(attributes, AZURE_CLIENT_NAME, null); + if ((StringUtil.isEmpty(azureResourceGroup) || StringUtil.isEmpty(azureClientName))) { + throw new ResourceException(ResourceException.BAD_REQUEST, "must specify azureClientId, or azureResourceGroup and azureClientName"); + } + try { + requestAzureClientId = getClientId(domainDetails.getAzureSubscription(), azureResourceGroup, + azureClientName, systemAccessToken.getAttributes().get(AZURE_ACCESS_TOKEN)); + } catch (Exception ex) { + throw new ResourceException(ResourceException.FORBIDDEN, ex.getMessage()); + } + } + } + + // Verify that the given principal is authorized for all scopes requested + + String azureTokenScope = getRequestAttribute(attributes, AZURE_TOKEN_SCOPE, AZURE_MGMT_SCOPE); + for (String scopeItem : azureTokenScope.split(" ")) { + String resource = domainDetails.getName() + ":" + scopeItem; + if (!authorizer.access(AZURE_SCOPE_ACTION, resource, principal, null)) { + throw new ResourceException(ResourceException.FORBIDDEN, "Principal not authorized for configured scope"); + } + } + + // Now exchange the ID token of the requested role for an Azure access token for the requested client + + try { + idToken.setSubject(idTokenGroups.get(0)); + final String signedIdToken = idTokenSigner.sign(idToken, "rsa"); + AccessTokenResponse accessToken = getAccessToken(azureProviderIdentityLoginUri, signedIdToken, requestAzureClientId, azureTokenScope); + return createResponse(accessToken, domainDetails); + } catch (Exception ex) { + throw new ResourceException(ResourceException.FORBIDDEN, ex.getMessage()); + } + } + + private ExternalCredentialsResponse getSystemAccessToken(String azureProviderIdentityLoginUri, DomainDetails domain, IdToken itToken, IdTokenSigner signer) { + ExternalCredentialsResponse cached = systemAccessTokenCache.get(domain.getAzureClient()); + if (cached != null && cached.getExpiration().millis() > System.currentTimeMillis() + 300_000) { + return cached; + } + try { + itToken.setSubject(SYSTEM_AZURE_CLIENT_ROLE); + final String signedIdToken = signer.sign(itToken, "rsa"); + AccessTokenResponse accessToken = getAccessToken(azureProviderIdentityLoginUri, signedIdToken, domain.getAzureClient(), AZURE_MGMT_SCOPE); + ExternalCredentialsResponse response = createResponse(accessToken, domain); + systemAccessTokenCache.put(domain.getAzureClient(), response); + return response; + } catch (Exception ex) { + throw new ResourceException(ResourceException.FORBIDDEN, ex.getMessage()); + } + } + + private AccessTokenResponse getAccessToken(String azureProviderIdentityLoginUri, String signedIdToken, String azureClientId, String azureTokenScope) { + try { + List params = new ArrayList<>(); + params.add(new BasicNameValuePair("scope", azureTokenScope)); + params.add(new BasicNameValuePair("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")); + params.add(new BasicNameValuePair("client_assertion", signedIdToken)); + params.add(new BasicNameValuePair("client_id", azureClientId)); + params.add(new BasicNameValuePair("grant_type", "client_credentials")); + HttpPost httpPost = new HttpPost(azureProviderIdentityLoginUri); + httpPost.setEntity(new UrlEncodedFormEntity(params)); + HttpDriverResponse httpResponse = httpDriver.doPostHttpResponse(httpPost); + + if (httpResponse.getStatusCode() != HttpStatus.SC_OK) { + String errorMessage = jsonMapper.readTree(httpResponse.getMessage()).get("error_description").asText(); + throw new ResourceException(httpResponse.getStatusCode(), errorMessage); + } + return jsonMapper.readValue(httpResponse.getMessage(), AccessTokenResponse.class); + } catch (Exception ex) { + throw new ResourceException(ResourceException.FORBIDDEN, ex.getMessage()); + } + } + + private ExternalCredentialsResponse createResponse(AccessTokenResponse accessToken, DomainDetails domainDetails) { + ExternalCredentialsResponse response = new ExternalCredentialsResponse(); + Map attributes = new HashMap<>(); + attributes.put(AZURE_ACCESS_TOKEN, accessToken.getAccess_token()); + attributes.put(AZURE_SUBSCRIPTION, domainDetails.getAzureSubscription()); + attributes.put(AZURE_TENANT, domainDetails.getAzureTenant()); + response.setAttributes(attributes); + response.setExpiration(Timestamp.fromMillis(System.currentTimeMillis() + 1000L * accessToken.getExpires_in())); + return response; + } + + private String getClientId(String azureSubscription, String azureResourceGroup, String azureClientName, String accessToken) throws IOException { + String userManagedIdentityUrl = AZURE_MGMT_URL + + "/subscriptions/" + azureSubscription + + "/resourceGroups/" + azureResourceGroup + + "/providers/Microsoft.ManagedIdentity/userAssignedIdentities/" + azureClientName + + "?api-version=2023-01-31"; + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + accessToken); + + String response = httpDriver.doGet(userManagedIdentityUrl, headers); + if (StringUtil.isEmpty(response)) { + throw new ResourceException(ResourceException.FORBIDDEN, "Unable to retrieve Azure client ID"); + } + + AzureUserManagedIdentityResponse userManagedIdentityResponse = jsonMapper.readValue(response, AzureUserManagedIdentityResponse.class); + String clientId = userManagedIdentityResponse.getProperties().getClientId(); + if (StringUtil.isEmpty(clientId)) { + throw new ResourceException(ResourceException.FORBIDDEN, "Unable to retrieve Azure client ID"); + } + return clientId; + } + +} diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/external/azure/AzureUserManagedIdentityResponse.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/external/azure/AzureUserManagedIdentityResponse.java new file mode 100644 index 00000000000..aeaeb9ad71c --- /dev/null +++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/external/azure/AzureUserManagedIdentityResponse.java @@ -0,0 +1,48 @@ +/* + * Copyright The Athenz Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yahoo.athenz.zts.external.azure; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +// https://learn.microsoft.com/en-us/rest/api/managedidentity/user-assigned-identities/get?view=rest-managedidentity-2023-01-31&tabs=HTTP +public class AzureUserManagedIdentityResponse { + + private Properties properties; + + public Properties getProperties() { + return properties; + } + + public void setProperties(Properties properties) { + this.properties = properties; + } + + public static class Properties { + + private String clientId; + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + } + +} diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/external/gcp/GcpAccessTokenProvider.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/external/gcp/GcpAccessTokenProvider.java index cae95c53ba3..7773d11752e 100644 --- a/servers/zts/src/main/java/com/yahoo/athenz/zts/external/gcp/GcpAccessTokenProvider.java +++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/external/gcp/GcpAccessTokenProvider.java @@ -19,7 +19,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.yahoo.athenz.auth.Authorizer; import com.yahoo.athenz.auth.Principal; +import com.yahoo.athenz.auth.token.IdToken; import com.yahoo.athenz.common.server.external.ExternalCredentialsProvider; +import com.yahoo.athenz.common.server.external.IdTokenSigner; import com.yahoo.athenz.common.server.http.HttpDriver; import com.yahoo.athenz.common.server.http.HttpDriverResponse; import com.yahoo.athenz.common.server.rest.ResourceException; @@ -133,15 +135,10 @@ GcpExchangeTokenResponse getExchangeToken(DomainDetails domainDetails, final Str * First, we're going to get our exchange token based on our ZTS ID token * and then request an access token for the given scope as described in the GCP docs: * https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken - * @param principal Principal making the request - * @param domainDetails Domain details including cloud info - * @param idToken signed jwt id token - * @param externalCredentialsRequest request attributes - * @return GcpExchangeTokenResponse which contains the exchange token - * @throws ResourceException in case of any errors */ @Override - public ExternalCredentialsResponse getCredentials(Principal principal, DomainDetails domainDetails, String idToken, ExternalCredentialsRequest externalCredentialsRequest) + public ExternalCredentialsResponse getCredentials(Principal principal, DomainDetails domainDetails, List idTokenGroups, + IdToken idToken, IdTokenSigner idTokenSigner, ExternalCredentialsRequest externalCredentialsRequest) throws ResourceException { // first make sure that our required components are available and configured @@ -149,6 +146,12 @@ public ExternalCredentialsResponse getCredentials(Principal principal, DomainDet if (authorizer == null) { throw new ResourceException(ResourceException.FORBIDDEN, "ZTS authorizer not configured"); } + if (StringUtil.isEmpty(domainDetails.getGcpProjectId())) { + throw new ResourceException(ResourceException.FORBIDDEN, "gcp project id not configured for domain"); + } + if (StringUtil.isEmpty(domainDetails.getGcpProjectNumber())) { + throw new ResourceException(ResourceException.FORBIDDEN, "gcp project number not configured for domain"); + } Map attributes = externalCredentialsRequest.getAttributes(); final String gcpServiceAccount = getRequestAttribute(attributes, GCP_SERVICE_ACCOUNT, null); @@ -167,10 +170,17 @@ public ExternalCredentialsResponse getCredentials(Principal principal, DomainDet } } + // Set the requested groups as the groups claim in the signed id token + + idToken.setSubject(principal.getFullName()); + idToken.setAudience(externalCredentialsRequest.getClientId()); + idToken.setGroups(idTokenGroups); + String signedIdToken = idTokenSigner.sign(idToken, null); + try { // first we're going to get our exchange token - GcpExchangeTokenResponse exchangeTokenResponse = getExchangeToken(domainDetails, idToken, externalCredentialsRequest); + GcpExchangeTokenResponse exchangeTokenResponse = getExchangeToken(domainDetails, signedIdToken, externalCredentialsRequest); final String serviceUrl = String.format("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s@%s.iam.gserviceaccount.com:generateAccessToken", gcpServiceAccount, domainDetails.getGcpProjectId()); diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/store/CloudStore.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/store/CloudStore.java index d16f00fe4d1..e8cda2f62b4 100644 --- a/servers/zts/src/main/java/com/yahoo/athenz/zts/store/CloudStore.java +++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/store/CloudStore.java @@ -71,6 +71,8 @@ public class CloudStore { BasicSessionCredentials credentials; final private Map awsAccountCache; final private Map azureSubscriptionCache; + final private Map azureTenantCache; + final private Map azureClientCache; final private Map gcpProjectIdCache; final private Map gcpProjectNumberCache; @@ -91,6 +93,8 @@ public CloudStore() { // initialize azure cache azureSubscriptionCache = new ConcurrentHashMap<>(); + azureTenantCache = new ConcurrentHashMap<>(); + azureClientCache = new ConcurrentHashMap<>(); // initialize gcp cache @@ -648,6 +652,14 @@ public String getAzureSubscription(String domainName) { return azureSubscriptionCache.get(domainName); } + public String getAzureTenant(String domainName) { + return azureTenantCache.get(domainName); + } + + public String getAzureClient(String domainName) { + return azureClientCache.get(domainName); + } + public String getGCPProjectId(String domainName) { return gcpProjectIdCache.get(domainName); } @@ -670,7 +682,7 @@ void updateAwsAccount(final String domainName, final String awsAccount) { } } - void updateAzureSubscription(final String domainName, final String azureSubscription) { + void updateAzureSubscription(final String domainName, final String azureSubscription, final String azureTenant, final String azureClient) { /* if we have a value specified for the domain, then we're just * going to insert it into our map and update the record. If @@ -679,8 +691,16 @@ void updateAzureSubscription(final String domainName, final String azureSubscrip if (!StringUtil.isEmpty(azureSubscription)) { azureSubscriptionCache.put(domainName, azureSubscription); + if (!StringUtil.isEmpty(azureTenant)) { + azureTenantCache.put(domainName, azureTenant); + } + if (!StringUtil.isEmpty(azureClient)) { + azureClientCache.put(domainName, azureClient); + } } else if (azureSubscriptionCache.get(domainName) != null) { azureSubscriptionCache.remove(domainName); + azureTenantCache.remove(domainName); + azureClientCache.remove(domainName); } } diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/store/DataStore.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/store/DataStore.java index 3e7dbe5813f..0ff97c84ea2 100644 --- a/servers/zts/src/main/java/com/yahoo/athenz/zts/store/DataStore.java +++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/store/DataStore.java @@ -1454,7 +1454,7 @@ public void addDomainToCache(String name, DataCache dataCache) { if (getCloudStore() != null) { getCloudStore().updateAwsAccount(name, dataCache.getDomainData().getAccount()); - getCloudStore().updateAzureSubscription(name, dataCache.getDomainData().getAzureSubscription()); + getCloudStore().updateAzureSubscription(name, dataCache.getDomainData().getAzureSubscription(), dataCache.getDomainData().getAzureTenant(), dataCache.getDomainData().getAzureClient()); getCloudStore().updateGCPProject(name, dataCache.getDomainData().getGcpProject(), dataCache.getDomainData().getGcpProjectNumber()); } diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplTest.java index daeb1575e73..ccc32133cca 100644 --- a/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplTest.java +++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplTest.java @@ -31,6 +31,7 @@ import com.yahoo.athenz.common.server.cert.Priority; import com.yahoo.athenz.common.server.cert.X509CertRecord; import com.yahoo.athenz.common.server.dns.HostnameResolver; +import com.yahoo.athenz.common.server.external.ExternalCredentialsProvider; import com.yahoo.athenz.common.server.http.HttpDriver; import com.yahoo.athenz.common.server.http.HttpDriverResponse; import com.yahoo.athenz.common.server.rest.Http; @@ -14264,6 +14265,8 @@ public void testGenerateInstanceConfirmObjectWithCtxCert() throws IOException { DataStore store = new DataStore(structStore, null, ztsMetric); Mockito.when(mockCloudStore.getAzureSubscription("athenz")).thenReturn("12345"); + Mockito.when(mockCloudStore.getAzureTenant("athenz")).thenReturn("33333"); + Mockito.when(mockCloudStore.getAzureClient("athenz")).thenReturn("54321"); Mockito.when(mockCloudStore.getGCPProjectId("athenz")).thenReturn(null); ZTSImpl ztsImpl = new ZTSImpl(mockCloudStore, store); @@ -14296,6 +14299,9 @@ public void testGenerateInstanceConfirmObjectWithCtxCert() throws IOException { assertEquals(confirmation.getAttributes().get(InstanceProvider.ZTS_INSTANCE_CERT_ISSUER_DN), "CN=self.signer.root"); assertEquals(confirmation.getAttributes().get(InstanceProvider.ZTS_INSTANCE_CERT_SUBJECT_DN), "CN=athenz.production,OU=Testing Domain,O=Athenz,L=LA,ST=CA,C=US"); assertEquals(confirmation.getAttributes().get(InstanceProvider.ZTS_INSTANCE_CERT_RSA_MOD_HASH), "72332cafbe1f874b4d89f6277508d03494c0dd4258e32a6999a7b8328eaa0e07"); + assertEquals(confirmation.getAttributes().get(InstanceProvider.ZTS_INSTANCE_AZURE_SUBSCRIPTION), "12345"); + assertEquals(confirmation.getAttributes().get(InstanceProvider.ZTS_INSTANCE_AZURE_TENANT), "33333"); + assertEquals(confirmation.getAttributes().get(InstanceProvider.ZTS_INSTANCE_AZURE_CLIENT), "54321"); // Ensure the cert issuer/key modulus/subject attributes are empty, when the context doesn't have certificates // Mocking is set up to return null for certs on a second call @@ -14522,12 +14528,10 @@ private ServerPrivateKey getServerPrivateKey(ZTSImpl ztsImpl, final String keyTy } @Test - public void testPostExternalCredentials() throws IOException { + public void testPostExternalCredentialsGcp() throws IOException { System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem"); - CloudStore cloudStore = new CloudStore(); - cloudStore.setHttpClient(null); ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); ztsImpl.userDomain = "user_domain"; @@ -14539,6 +14543,7 @@ public void testPostExternalCredentials() throws IOException { ResourceContext context = createResourceContext(principal); SignedDomain signedDomain = createSignedDomain("coretech", "sports", "api", true, null); + signedDomain.setDomain(signedDomain.getDomain().setGcpProject("project").setGcpProjectNumber("321")); store.processSignedDomain(signedDomain, false); GcpAccessTokenProvider provider = new GcpAccessTokenProvider(); @@ -14602,6 +14607,143 @@ public void testPostExternalCredentials() throws IOException { assertTrue(ex.getMessage().contains("gcp exchange token error")); } } + + @Test + public void testPostExternalCredentialsAzure() throws IOException { + + System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem"); + + ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store); + ztsImpl.userDomain = "user_domain"; + + // set back to our zts rsa private key + System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem"); + + Principal principal = SimplePrincipal.create("user_domain", "user", + "v=U1;d=user_domain;n=user;s=signature", 0, null); + + List readers = new ArrayList<>(); + readers.add(new RoleMember().setMemberName("user_domain.user")); + SignedDomain signedDomain = createSignedDomain("coretech", "sports", "api", new ArrayList<>(), readers, false, null); + signedDomain.setDomain(signedDomain.getDomain().setAzureSubscription("azsub").setAzureTenant("aztenant").setAzureClient("azclient")); + store.processSignedDomain(signedDomain, false); + + // Set up the athenz.azure domain, with the system azure client, and a provider as member + + { + SignedDomain systemDomain = new SignedDomain(); + + List roles = new ArrayList<>(); + Role role = new Role(); + role.setName("athenz.azure:role.azure-client"); + List members = new ArrayList<>(); + members.add(new RoleMember().setMemberName("athenz.azure.eastus")); + role.setRoleMembers(members); + roles.add(role); + + List policies = new ArrayList<>(); + com.yahoo.athenz.zms.DomainPolicies domainPolicies = new com.yahoo.athenz.zms.DomainPolicies(); + domainPolicies.setDomain("athenz.azure"); + domainPolicies.setPolicies(policies); + + com.yahoo.athenz.zms.SignedPolicies signedPolicies = new com.yahoo.athenz.zms.SignedPolicies(); + signedPolicies.setContents(domainPolicies); + signedPolicies.setSignature(Crypto.sign(SignUtils.asCanonicalString(domainPolicies), privateKey)); + signedPolicies.setKeyId("0"); + + DomainData domain = new DomainData(); + domain.setName("athenz.azure"); + domain.setRoles(roles); + domain.setPolicies(signedPolicies); + domain.setModified(Timestamp.fromCurrentTime()); + + systemDomain.setDomain(domain); + + systemDomain.setSignature(Crypto.sign(SignUtils.asCanonicalString(domain), privateKey)); + systemDomain.setKeyId("0"); + ztsImpl.dataStore.processSignedDomain(systemDomain, false); + } + + ExternalCredentialsProvider provider = Mockito.mock(ExternalCredentialsProvider.class); + ztsImpl.externalCredentialsManager.setProvider("azure", provider); + ztsImpl.externalCredentialsManager.enableProvider("azure"); + + // get external credentials for the configured domain, for the user principal + + ExternalCredentialsRequest extCredsRequest = new ExternalCredentialsRequest(); + Map attributes = new HashMap<>(); + attributes.put("athenzRoleName", "readers"); + attributes.put("azureClientID", "bad-c0ffee"); + extCredsRequest.setAttributes(attributes); + extCredsRequest.setClientId("coretech.api"); + + ResourceContext context = createResourceContext(principal); + + // Set up the mock provider to verify arguments, and return different tokens for different roles. + + ExternalCredentialsResponse response = new ExternalCredentialsResponse(); + response.setAttributes(new HashMap<>()); + Mockito.when(provider.getCredentials(any(), + assertArg(arg -> { + assertEquals(arg.getAzureSubscription(), "azsub"); + assertEquals(arg.getAzureTenant(), "aztenant"); + assertEquals(arg.getAzureClient(), "azclient"); + }), + assertArg(arg -> { + if (arg.get(0).equals("coretech:role.readers")) { + response.getAttributes().put("accessToken", "access-token-readers"); + } else if (arg.get(0).equals("athenz.azure:role.azure-client")) { + response.getAttributes().put("accessToken", "access-token-system"); + } else { + fail(); + } + assertEquals(arg.size(), 1); + }), + assertArg(arg -> { + assertEquals(arg.getIssuer(), ztsImpl.ztsOpenIDIssuer); + assertEquals(arg.getVersion(), 1); + }), + any(), + any())).thenReturn(response); + + ExternalCredentialsResponse extCredsResponse = ztsImpl.postExternalCredentialsRequest(context, + "azure", "coretech", extCredsRequest); + assertEquals(extCredsResponse.getAttributes().get("accessToken"), "access-token-readers"); + + // now let's test the same api through our instance provider + + InstanceExternalCredentialsProvider extCredsProvider = new InstanceExternalCredentialsProvider("user_domain.user", ztsImpl); + extCredsResponse = extCredsProvider.getExternalCredentials("azure", "coretech", extCredsRequest); + assertEquals(extCredsResponse.getAttributes().get("accessToken"), "access-token-readers"); + + // get external credentials fails when the requested role is writers, where the principal is not a member + + extCredsRequest.getAttributes().put("athenzRoleName", "writers"); + try { + ztsImpl.postExternalCredentialsRequest(context, "azure", "coretech", extCredsRequest); + fail(); + } catch (ResourceException ex) { + assertEquals(403, ex.getCode()); + assertTrue(ex.getMessage().contains("principal not included in requested roles")); + } + + // user cannot get the system access token, as it's not a member of the system azure client role + extCredsRequest.getAttributes().remove("athenzRoleName"); + extCredsRequest.getAttributes().put("athenzScope", "openid athenz.azure:role.azure-client"); + try { + ztsImpl.postExternalCredentialsRequest(context, "azure", "coretech", extCredsRequest); + fail(); + } catch (ResourceException ex) { + assertEquals(403, ex.getCode()); + assertTrue(ex.getMessage().contains("principal not included in requested roles")); + } + + // the provider can get the system access token for "coretech", through the instance credentials provider + + InstanceExternalCredentialsProvider providerExtCredsProvider = new InstanceExternalCredentialsProvider("athenz.azure.eastus", ztsImpl); + extCredsResponse = providerExtCredsProvider.getExternalCredentials("azure", "coretech", extCredsRequest); + assertEquals(extCredsResponse.getAttributes().get("accessToken"), "access-token-system"); + } @Test public void testPostExternalCredentialsFailures() throws IOException { diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/external/azure/AzureAccessTokenProviderTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/external/azure/AzureAccessTokenProviderTest.java new file mode 100644 index 00000000000..6a2005ff0d4 --- /dev/null +++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/external/azure/AzureAccessTokenProviderTest.java @@ -0,0 +1,421 @@ +/* + * Copyright The Athenz Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yahoo.athenz.zts.external.azure; + +import com.yahoo.athenz.auth.Authorizer; +import com.yahoo.athenz.auth.Principal; +import com.yahoo.athenz.auth.token.IdToken; +import com.yahoo.athenz.auth.token.OAuth2Token; +import com.yahoo.athenz.common.server.external.IdTokenSigner; +import com.yahoo.athenz.common.server.http.HttpDriver; +import com.yahoo.athenz.common.server.http.HttpDriverResponse; +import com.yahoo.athenz.common.server.rest.ResourceException; +import com.yahoo.athenz.zts.DomainDetails; +import com.yahoo.athenz.zts.ExternalCredentialsRequest; +import com.yahoo.athenz.zts.ExternalCredentialsResponse; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.same; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +public class AzureAccessTokenProviderTest { + + public static final String ACCESS_TOKEN_ERROR_RESPONSE = "{\n" + + " \"error\": \"invalid_client\",\n" + + " \"error_description\": \"AADSTS700212: No matching federated identity record found for presented assertion audience 'my.audience'. Please check your federated identity credential Subject, Audience and Issuer against the presented assertion. https://docs.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation Trace ID: c31234bc-69fd-4797-bbc5-809e7afb5000 Correlation ID: 1b321183-4fb9-4116-bd51-5700f7fbdf0c Timestamp: 2024-04-16 06:58:38Z\",\n" + + " \"error_codes\": [\n" + + " 700212\n" + + " ],\n" + + " \"timestamp\": \"2024-04-16 06:58:38Z\",\n" + + " \"trace_id\": \"c3321110-69fd-4797-bbc5-809e7afb5000\",\n" + + " \"correlation_id\": \"13211683-4fb9-4116-bd51-5700f7fbdf0c\"\n" + + "}"; + public static final String ACCESS_TOKEN_RESPONSE_STR = + "{\n" + + " \"token_type\": \"Bearer\",\n" + + " \"expires_in\": 86399,\n" + + " \"ext_expires_in\": 86399,\n" + + " \"access_token\": \"access-token\"\n" + + "}"; + public static final String USER_MANAGED_IDENTITY_INCOMPLETE_RESPONSE_STR = + "{\n" + + " \"id\": \"/subscriptions/subid/resourcegroups/rgName/providers/Microsoft.ManagedIdentity/userAssignedIdentities/identityName\",\n" + + " \"location\": \"eastus\",\n" + + " \"name\": \"identityName\",\n" + + " \"properties\": {\n" + + " \"principalId\": \"25cc773c-7f05-40fc-a104-32d2300754ad\",\n" + + " \"tenantId\": \"b6c948ef-f6b5-4384-8354-da3a15eca969\"\n" + + " },\n" + + " \"tags\": {\n" + + " \"key1\": \"value1\",\n" + + " \"key2\": \"value2\"\n" + + " },\n" + + " \"type\": \"Microsoft.ManagedIdentity/userAssignedIdentities\"\n" + + "}"; + public static final String USER_MANAGED_IDENTITY_RESPONSE_STR = + "{\n" + + " \"id\": \"/subscriptions/subid/resourcegroups/rgName/providers/Microsoft.ManagedIdentity/userAssignedIdentities/identityName\",\n" + + " \"location\": \"eastus\",\n" + + " \"name\": \"identityName\",\n" + + " \"properties\": {\n" + + " \"clientId\": \"request-azure-client-id\",\n" + + " \"principalId\": \"25cc773c-7f05-40fc-a104-32d2300754ad\",\n" + + " \"tenantId\": \"b6c948ef-f6b5-4384-8354-da3a15eca969\"\n" + + " },\n" + + " \"tags\": {\n" + + " \"key1\": \"value1\",\n" + + " \"key2\": \"value2\"\n" + + " },\n" + + " \"type\": \"Microsoft.ManagedIdentity/userAssignedIdentities\"\n" + + "}"; + static final IdTokenSigner signer = (idToken, keyType) -> idToken.getSubject(); + + @Test + public void testAzureAccessTokenProviderFailures() throws IOException { + + AzureAccessTokenProvider provider = new AzureAccessTokenProvider(); + Principal principal = Mockito.mock(Principal.class); + DomainDetails domainDetails = new DomainDetails(); + List idTokenGroups = new ArrayList<>(); + ExternalCredentialsRequest request = new ExternalCredentialsRequest(); + Map attributes = new HashMap<>(); + request.setAttributes(attributes); + + // authorizer not configured + + try { + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.FORBIDDEN); + assertTrue(ex.getMessage().contains("ZTS authorizer not configured")); + } + Authorizer authorizer = Mockito.mock(Authorizer.class); + provider.setAuthorizer(authorizer); + + // azure tenant not present on domain + + try { + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.FORBIDDEN); + assertTrue(ex.getMessage().contains("azure tenant not configured for domain")); + } + domainDetails.setAzureTenant("az-tenant"); + + // azure client not set for domain + + try { + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.FORBIDDEN); + assertTrue(ex.getMessage().contains("azure client not configured for domain")); + } + domainDetails.setAzureClient("az-client"); + + // no roles accessible to principal + + try { + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); + assertTrue(ex.getMessage().contains("must specify exactly one accessible role")); + } + idTokenGroups.add("domain:role.client"); + + // http driver failing to get access token for the athenz azure client, for reading the request azure client id + + HttpDriver httpDriver = Mockito.mock(HttpDriver.class); + provider.setHttpDriver(httpDriver); + Mockito.when(httpDriver.doPostHttpResponse(any())).thenReturn(new HttpDriverResponse(401, ACCESS_TOKEN_ERROR_RESPONSE, null)); + try { + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); + fail(); + } catch (ResourceException ex) { + assertEquals(ResourceException.FORBIDDEN, ex.getCode()); + assertTrue(ex.getMessage().contains("assertion audience 'my.audience'")); + } + + // http driver failing to get access token for the system azure client, when that is requested + + List systemIdTokenGroups = new ArrayList<>(); + systemIdTokenGroups.add("athenz.azure:role.azure-client"); + try { + provider.getCredentials(principal, domainDetails, systemIdTokenGroups, new IdToken(), signer, request); + fail(); + } catch (ResourceException ex) { + assertEquals(ResourceException.FORBIDDEN, ex.getCode()); + assertTrue(ex.getMessage().contains("assertion audience 'my.audience'")); + } + Mockito.when(httpDriver.doPostHttpResponse(any())).thenReturn(new HttpDriverResponse(200, ACCESS_TOKEN_RESPONSE_STR, null)); + + // azure client id not indicated, and not system access token request + + try { + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); + assertTrue(ex.getMessage().contains("must specify azureClientId, or azureResourceGroup and azureClientName")); + } + attributes.put("azureResourceGroup", "group"); + attributes.put("azureClientName", "client"); + + // http driver failing to read the request azure client id, from resource group and client names + + Mockito.when(httpDriver.doGet(any(), any())).thenReturn(""); + try { + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); + fail(); + } catch (ResourceException ex) { + assertEquals(ResourceException.FORBIDDEN, ex.getCode()); + assertTrue(ex.getMessage().contains("Unable to retrieve Azure client ID")); + } + + // http driver returning incomplete response for user managed identity, from resource group and client names + + Mockito.when(httpDriver.doGet(any(), any())).thenReturn(USER_MANAGED_IDENTITY_INCOMPLETE_RESPONSE_STR); + try { + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); + fail(); + } catch (ResourceException ex) { + assertEquals(ResourceException.FORBIDDEN, ex.getCode()); + assertTrue(ex.getMessage().contains("Unable to retrieve Azure client ID")); + } + Mockito.when(httpDriver.doGet(any(), any())).thenReturn(USER_MANAGED_IDENTITY_RESPONSE_STR); + + // not authorized for requested scope + + Mockito.when(authorizer.access(any(), any(), any(), any())).thenReturn(false); + try { + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); + fail(); + } catch (ResourceException ex) { + assertEquals(ResourceException.FORBIDDEN, ex.getCode()); + assertTrue(ex.getMessage().contains("Principal not authorized for configured scope")); + } + Mockito.when(authorizer.access(any(), any(), any(), any())).thenReturn(true); + + // http driver returning error response when attempting to get the requested access token + + attributes.put("azureClientId", "az-client-id"); // skip getting the client ID + Mockito.when(httpDriver.doPostHttpResponse(any())).thenReturn(new HttpDriverResponse(401, ACCESS_TOKEN_ERROR_RESPONSE, null)); + try { + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); + fail(); + } catch (ResourceException ex) { + assertEquals(ResourceException.FORBIDDEN, ex.getCode()); + assertTrue(ex.getMessage().contains("assertion audience 'my.audience'")); + } + + // http driver returning failure + + Mockito.when(httpDriver.doPostHttpResponse(any())).thenThrow(new IOException("my http-failure")); + try { + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); + fail(); + } catch (ResourceException ex) { + assertEquals(ResourceException.FORBIDDEN, ex.getCode()); + assertTrue(ex.getMessage().contains("my http-failure")); + } + } + + @Test + public void testAzureAccessTokenProvider() throws IOException { + + AzureAccessTokenProvider provider = new AzureAccessTokenProvider(); + + List idTokenGroups = new ArrayList<>(); + idTokenGroups.add("my-domain:role.client"); + IdToken idToken = new IdToken(); + Principal principal = Mockito.mock(Principal.class); + DomainDetails domainDetails = new DomainDetails() + .setName("my-domain") + .setAzureSubscription("azure-subscription") + .setAzureTenant("azure-tenant") + .setAzureClient("athenz-azure-client-id"); + + ExternalCredentialsRequest request = new ExternalCredentialsRequest(); + Map attributes = new HashMap<>(); + attributes.put("azureResourceGroup", "azure-resource-group"); + attributes.put("azureClientName", "azure-client-name"); + attributes.put("azureTokenScope", "my/scope"); + request.setAttributes(attributes); + + Authorizer authorizer = Mockito.mock(Authorizer.class); + provider.setAuthorizer(authorizer); + + HttpDriverResponse accessTokenResponse = new HttpDriverResponse(200, ACCESS_TOKEN_RESPONSE_STR, null); + HttpDriver httpDriver = Mockito.mock(HttpDriver.class); + + List expectedScopes = new ArrayList<>(); + List expectedClientIds = new ArrayList<>(); + List expectedIdTokens = new ArrayList<>(); + + // first access token request is for the athenz azure client to look up the client id + + expectedScopes.add("https%3A%2F%2Fmanagement.azure.com%2F.default"); + expectedClientIds.add("athenz-azure-client-id"); + expectedIdTokens.add("athenz.azure%3Arole.azure-client"); + + // second access token request is for the requested access token + + expectedScopes.add("my%2Fscope"); + expectedClientIds.add("request-azure-client-id"); + expectedIdTokens.add("my-domain%3Arole.client"); + + Mockito.when(httpDriver.doPostHttpResponse(assertArg(arg -> { + assertEquals(arg.getURI(), URI.create("https://login.microsoftonline.com/azure-tenant/oauth2/v2.0/token")); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + arg.getEntity().writeTo(out); + String body = out.toString(); + assertEquals(body, + "scope=" + expectedScopes.remove(0) + "&" + + "client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&" + + "client_assertion=" + expectedIdTokens.remove(0) + "&" + + "client_id=" + expectedClientIds.remove(0) + "&" + + "grant_type=client_credentials"); + }))).thenReturn(accessTokenResponse); + + String expectedUrl = "https://management.azure.com/subscriptions/azure-subscription/resourceGroups/azure-resource-group" + + "/providers/Microsoft.ManagedIdentity/userAssignedIdentities/azure-client-name?api-version=2023-01-31"; + Map expectedHeaders = new HashMap<>(); + expectedHeaders.put("Authorization", "Bearer access-token"); + Mockito.when(httpDriver.doGet(eq(expectedUrl), eq(expectedHeaders))).thenReturn(USER_MANAGED_IDENTITY_RESPONSE_STR); + + provider.setHttpDriver(httpDriver); + Mockito.when(authorizer.access(eq("azure.scope_access"), eq("my-domain:my/scope"), same(principal), isNull())).thenReturn(true); + + ExternalCredentialsResponse response = provider.getCredentials(principal, domainDetails, idTokenGroups, idToken, signer, request); + assertNotNull(response); + Map responseAttributes = response.getAttributes(); + assertEquals(responseAttributes.get("accessToken"), "access-token"); + assertEquals(responseAttributes.get("azureSubscription"), "azure-subscription"); + assertEquals(responseAttributes.get("azureTenant"), "azure-tenant"); + assertEquals(idToken.getSubject(), "my-domain:role.client"); + assertEquals(idToken.getAudience(), "api://AzureADTokenExchange"); + + assertEquals(expectedClientIds.size(), 0); + assertEquals(expectedIdTokens.size(), 0); + assertEquals(expectedScopes.size(), 0); + } + + + @Test + public void testAzureAccessTokenProviderSystemToken() throws IOException { + + AzureAccessTokenProvider provider = new AzureAccessTokenProvider(); + + List idTokenGroups = new ArrayList<>(); + idTokenGroups.add("athenz.azure:role.azure-client"); // System azure client role + IdToken idToken = new IdToken(); + Principal principal = Mockito.mock(Principal.class); + DomainDetails domainDetails1 = new DomainDetails() + .setName("domain1") + .setAzureSubscription("azure-subscription1") + .setAzureTenant("azure-tenant1") + .setAzureClient("athenz-azure-client-id1"); + DomainDetails domainDetails2 = new DomainDetails() + .setName("domain2") + .setAzureSubscription("azure-subscription2") + .setAzureTenant("azure-tenant2") + .setAzureClient("athenz-azure-client-id2"); + + ExternalCredentialsRequest request = new ExternalCredentialsRequest(); + request.setAttributes(new HashMap<>()); + + provider.setAuthorizer(Mockito.mock(Authorizer.class)); + + HttpDriverResponse accessTokenResponse = new HttpDriverResponse(200, ACCESS_TOKEN_RESPONSE_STR, null); + HttpDriver httpDriver = Mockito.mock(HttpDriver.class); + + List expectedURIs = new ArrayList<>(); + List expectedClientIds = new ArrayList<>(); + + // first access token request is for the system azure client for the first domain + + expectedURIs.add("https://login.microsoftonline.com/azure-tenant1/oauth2/v2.0/token"); + expectedClientIds.add("athenz-azure-client-id1"); + + // second access token request is for the system azure client for the second domain + + expectedURIs.add("https://login.microsoftonline.com/azure-tenant2/oauth2/v2.0/token"); + expectedClientIds.add("athenz-azure-client-id2"); + + Mockito.when(httpDriver.doPostHttpResponse(assertArg(arg -> { + assertEquals(arg.getURI(), URI.create(expectedURIs.remove(0))); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + arg.getEntity().writeTo(out); + String body = out.toString(); + assertEquals(body, + "scope=https%3A%2F%2Fmanagement.azure.com%2F.default&" + + "client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&" + + "client_assertion=athenz.azure%3Arole.azure-client&" + + "client_id=" + expectedClientIds.remove(0) + "&" + + "grant_type=client_credentials"); + }))).thenReturn(accessTokenResponse); + + provider.setHttpDriver(httpDriver); + + // First request for system access token for first domain + + ExternalCredentialsResponse response1 = provider.getCredentials(principal, domainDetails1, idTokenGroups, idToken, signer, request); + assertNotNull(response1); + Map responseAttributes1 = response1.getAttributes(); + assertEquals(responseAttributes1.get("accessToken"), "access-token"); + assertEquals(responseAttributes1.get("azureSubscription"), "azure-subscription1"); + assertEquals(responseAttributes1.get("azureTenant"), "azure-tenant1"); + assertEquals(idToken.getSubject(), "athenz.azure:role.azure-client"); + assertEquals(idToken.getAudience(), "api://AzureADTokenExchange"); + + // Another request for system access token for first domain uses the cached result + ExternalCredentialsResponse response2 = provider.getCredentials(principal, domainDetails1, idTokenGroups, idToken, signer, request); + assertSame(response1, response2); + + // Third request is for the second domain + ExternalCredentialsResponse response3 = provider.getCredentials(principal, domainDetails2, idTokenGroups, idToken, signer, request); + assertNotNull(response3); + Map responseAttributes3 = response3.getAttributes(); + assertEquals(responseAttributes3.get("accessToken"), "access-token"); + assertEquals(responseAttributes3.get("azureSubscription"), "azure-subscription2"); + assertEquals(responseAttributes3.get("azureTenant"), "azure-tenant2"); + assertEquals(idToken.getSubject(), "athenz.azure:role.azure-client"); + assertEquals(idToken.getAudience(), "api://AzureADTokenExchange"); + + assertEquals(expectedClientIds.size(), 0); + assertEquals(expectedURIs.size(), 0); + } +} diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/external/gcp/GcpAccessTokenProviderTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/external/gcp/GcpAccessTokenProviderTest.java index feb1dbd0197..fca518580a4 100644 --- a/servers/zts/src/test/java/com/yahoo/athenz/zts/external/gcp/GcpAccessTokenProviderTest.java +++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/external/gcp/GcpAccessTokenProviderTest.java @@ -18,6 +18,8 @@ import com.yahoo.athenz.auth.Authorizer; import com.yahoo.athenz.auth.Principal; +import com.yahoo.athenz.auth.token.IdToken; +import com.yahoo.athenz.common.server.external.IdTokenSigner; import com.yahoo.athenz.common.server.http.HttpDriver; import com.yahoo.athenz.common.server.http.HttpDriverResponse; import com.yahoo.athenz.common.server.rest.ResourceException; @@ -28,10 +30,13 @@ import org.testng.annotations.Test; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; import static org.testng.Assert.*; public class GcpAccessTokenProviderTest { @@ -68,6 +73,7 @@ public class GcpAccessTokenProviderTest { " ]\n" + " }\n" + "}"; + static final IdTokenSigner signer = (idToken, keyType) -> "id-token"; @Test public void testGcpAccessTokenProviderFailures() throws IOException { @@ -75,7 +81,7 @@ public void testGcpAccessTokenProviderFailures() throws IOException { GcpAccessTokenProvider provider = new GcpAccessTokenProvider(); Principal principal = Mockito.mock(Principal.class); DomainDetails domainDetails = new DomainDetails(); - final String idToken = "id-token"; + List idTokenGroups = new ArrayList<>(); ExternalCredentialsRequest request = new ExternalCredentialsRequest(); Map attributes = new HashMap<>(); request.setAttributes(attributes); @@ -83,20 +89,32 @@ public void testGcpAccessTokenProviderFailures() throws IOException { // authorizer not configured try { - provider.getCredentials(principal, domainDetails, idToken, request); + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); fail(); } catch (ResourceException ex) { assertEquals(ResourceException.FORBIDDEN, ex.getCode()); assertTrue(ex.getMessage().contains("ZTS authorizer not configured")); } - // gcp service account not present + // gcp project not present Authorizer authorizer = Mockito.mock(Authorizer.class); provider.setAuthorizer(authorizer); try { - provider.getCredentials(principal, domainDetails, idToken, request); + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); + fail(); + } catch (ResourceException ex) { + assertEquals(ResourceException.FORBIDDEN, ex.getCode()); + assertTrue(ex.getMessage().contains("gcp project id not configured")); + } + + // gcp service account not present + + domainDetails.setGcpProjectId("gcp-project").setGcpProjectNumber("123"); + + try { + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); fail(); } catch (ResourceException ex) { assertEquals(ResourceException.BAD_REQUEST, ex.getCode()); @@ -106,10 +124,10 @@ public void testGcpAccessTokenProviderFailures() throws IOException { // not authorized attributes.put(GcpAccessTokenProvider.GCP_SERVICE_ACCOUNT, "gcp-service"); - Mockito.when(authorizer.access(any(), any(), any(), any())).thenReturn(false); + when(authorizer.access(any(), any(), any(), any())).thenReturn(false); try { - provider.getCredentials(principal, domainDetails, idToken, request); + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); fail(); } catch (ResourceException ex) { assertEquals(ResourceException.FORBIDDEN, ex.getCode()); @@ -119,12 +137,12 @@ public void testGcpAccessTokenProviderFailures() throws IOException { // http driver returning failure HttpDriver httpDriver = Mockito.mock(HttpDriver.class); - Mockito.when(httpDriver.doPostHttpResponse(any())).thenThrow(new IOException("http-failure")); + when(httpDriver.doPostHttpResponse(any())).thenThrow(new IOException("http-failure")); provider.setHttpDriver(httpDriver); - Mockito.when(authorizer.access(any(), any(), any(), any())).thenReturn(true); + when(authorizer.access(any(), any(), any(), any())).thenReturn(true); try { - provider.getCredentials(principal, domainDetails, idToken, request); + provider.getCredentials(principal, domainDetails, idTokenGroups, new IdToken(), signer, request); fail(); } catch (ResourceException ex) { assertEquals(ResourceException.FORBIDDEN, ex.getCode()); @@ -138,12 +156,17 @@ public void testGcpAccessTokenProvider() throws IOException { GcpAccessTokenProvider provider = new GcpAccessTokenProvider(); Principal principal = Mockito.mock(Principal.class); + when(principal.getFullName()).thenReturn("user.joe"); + IdToken idToken = new IdToken(); + List idTokenGroups = new ArrayList<>(); + idTokenGroups.add("domain:role.reader"); + idTokenGroups.add("domain:role.writer"); DomainDetails domainDetails = new DomainDetails() .setGcpProjectId("gcp-project") .setGcpProjectNumber("gcp-project-number"); - final String idToken = "id-token"; ExternalCredentialsRequest request = new ExternalCredentialsRequest(); + request.setClientId("domain.gcp"); request.setExpiryTime(1800); Map attributes = new HashMap<>(); attributes.put(GcpAccessTokenProvider.GCP_SERVICE_ACCOUNT, "gcp-service"); @@ -155,15 +178,18 @@ public void testGcpAccessTokenProvider() throws IOException { HttpDriver httpDriver = Mockito.mock(HttpDriver.class); HttpDriverResponse exchangeTokenResponse = new HttpDriverResponse(200, EXCHANGE_TOKEN_RESPONSE_STR, null); HttpDriverResponse accessTokenResponse = new HttpDriverResponse(200, ACCESS_TOKEN_RESPONSE_STR, null); - Mockito.when(httpDriver.doPostHttpResponse(any())).thenReturn(exchangeTokenResponse, accessTokenResponse); + when(httpDriver.doPostHttpResponse(any())).thenReturn(exchangeTokenResponse, accessTokenResponse); provider.setHttpDriver(httpDriver); - Mockito.when(authorizer.access(any(), any(), any(), any())).thenReturn(true); + when(authorizer.access(any(), any(), any(), any())).thenReturn(true); - ExternalCredentialsResponse response = provider.getCredentials(principal, domainDetails, idToken, request); + ExternalCredentialsResponse response = provider.getCredentials(principal, domainDetails, idTokenGroups, idToken, signer, request); assertNotNull(response); Map responseAttributes = response.getAttributes(); assertEquals(responseAttributes.get("accessToken"), "access-token"); + assertEquals(idToken.getSubject(), "user.joe"); + assertEquals(idToken.getAudience(), "domain.gcp"); + assertEquals(idToken.getGroups(), idTokenGroups); } @Test @@ -176,7 +202,6 @@ public void testGcpAccessTokenProviderExchangeTokenFailure() throws IOException .setGcpProjectId("gcp-project") .setGcpProjectNumber("gcp-project-number"); - final String idToken = "id-token"; ExternalCredentialsRequest request = new ExternalCredentialsRequest(); request.setExpiryTime(1800); Map attributes = new HashMap<>(); @@ -188,13 +213,13 @@ public void testGcpAccessTokenProviderExchangeTokenFailure() throws IOException HttpDriver httpDriver = Mockito.mock(HttpDriver.class); HttpDriverResponse exchangeTokenResponse = new HttpDriverResponse(401, EXCHANGE_TOKEN_ERROR_STR, null); - Mockito.when(httpDriver.doPostHttpResponse(any())).thenReturn(exchangeTokenResponse); + when(httpDriver.doPostHttpResponse(any())).thenReturn(exchangeTokenResponse); provider.setHttpDriver(httpDriver); - Mockito.when(authorizer.access(any(), any(), any(), any())).thenReturn(true); + when(authorizer.access(any(), any(), any(), any())).thenReturn(true); try { - provider.getCredentials(principal, domainDetails, idToken, request); + provider.getCredentials(principal, domainDetails, new ArrayList<>(), new IdToken(), signer, request); fail(); } catch (ResourceException ex) { assertEquals(403, ex.getCode()); @@ -212,7 +237,6 @@ public void testGcpAccessTokenProviderAccessTokenFailure() throws IOException { .setGcpProjectId("gcp-project") .setGcpProjectNumber("gcp-project-number"); - final String idToken = "id-token"; ExternalCredentialsRequest request = new ExternalCredentialsRequest(); Map attributes = new HashMap<>(); attributes.put(GcpAccessTokenProvider.GCP_SERVICE_ACCOUNT, "gcp-service"); @@ -224,13 +248,13 @@ public void testGcpAccessTokenProviderAccessTokenFailure() throws IOException { HttpDriver httpDriver = Mockito.mock(HttpDriver.class); HttpDriverResponse exchangeTokenResponse = new HttpDriverResponse(200, EXCHANGE_TOKEN_RESPONSE_STR, null); HttpDriverResponse accessTokenResponse = new HttpDriverResponse(403, ACCESS_TOKEN_ERROR_STR, null); - Mockito.when(httpDriver.doPostHttpResponse(any())).thenReturn(exchangeTokenResponse, accessTokenResponse); + when(httpDriver.doPostHttpResponse(any())).thenReturn(exchangeTokenResponse, accessTokenResponse); provider.setHttpDriver(httpDriver); - Mockito.when(authorizer.access(any(), any(), any(), any())).thenReturn(true); + when(authorizer.access(any(), any(), any(), any())).thenReturn(true); try { - provider.getCredentials(principal, domainDetails, idToken, request); + provider.getCredentials(principal, domainDetails, new ArrayList<>(), new IdToken(), signer, request); fail(); } catch (ResourceException ex) { assertEquals(403, ex.getCode()); diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/store/CloudStoreTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/store/CloudStoreTest.java index e9dd4c0c247..0240ae56a87 100644 --- a/servers/zts/src/test/java/com/yahoo/athenz/zts/store/CloudStoreTest.java +++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/store/CloudStoreTest.java @@ -1336,10 +1336,27 @@ public void testAWSCredentialsUpdaterExceptions () { public void testGetAzureSubscription() { CloudStore cloudStore = new CloudStore(); assertNull(cloudStore.getAzureSubscription("athenz")); - cloudStore.updateAzureSubscription("athenz", "12345"); + + cloudStore.updateAzureSubscription("athenz", "12345", "321", "999"); assertEquals("12345", cloudStore.getAzureSubscription("athenz")); - cloudStore.updateAzureSubscription("athenz", ""); + assertEquals("321", cloudStore.getAzureTenant("athenz")); + assertEquals("999", cloudStore.getAzureClient("athenz")); + + cloudStore.updateAzureSubscription("athenz", "", "", ""); assertNull(cloudStore.getAzureSubscription("athenz")); + assertNull(cloudStore.getAzureTenant("athenz")); + assertNull(cloudStore.getAzureClient("athenz")); + + cloudStore.updateAzureSubscription("athenz", "12345", null, "888"); + assertEquals("12345", cloudStore.getAzureSubscription("athenz")); + assertNull(cloudStore.getAzureTenant("athenz")); + assertEquals("888", cloudStore.getAzureClient("athenz")); + + cloudStore.updateAzureSubscription("athenz", "12345", "777", null); + assertEquals("12345", cloudStore.getAzureSubscription("athenz")); + assertEquals("777", cloudStore.getAzureTenant("athenz")); + assertEquals("888", cloudStore.getAzureClient("athenz")); + cloudStore.close(); } diff --git a/ui/docs/zms-api.md b/ui/docs/zms-api.md index 273b7348d66..976bff5e703 100644 --- a/ui/docs/zms-api.md +++ b/ui/docs/zms-api.md @@ -2139,6 +2139,18 @@ Set of metadata attributes that all domains may have and can be changed. "optional": true, "comment": "associated azure subscription id (system attribute - uniqueness check - if enabled)" }, + { + "name": "azureTenant", + "type": "String", + "optional": true, + "comment": "associated azure tenant id (system attribute)" + }, + { + "name": "azureClient", + "type": "String", + "optional": true, + "comment": "associated azure client id (system attribute)" + }, { "name": "gcpProject", "type": "String", diff --git a/ui/src/config/zms.json b/ui/src/config/zms.json index b2a9ac664ab..b2aa4888854 100644 --- a/ui/src/config/zms.json +++ b/ui/src/config/zms.json @@ -324,6 +324,18 @@ "optional": true, "comment": "associated azure subscription id (system attribute - uniqueness check - if enabled)" }, + { + "name": "azureTenant", + "type": "String", + "optional": true, + "comment": "associated azure tenant id (system attribute)" + }, + { + "name": "azureClient", + "type": "String", + "optional": true, + "comment": "associated azure client id (system attribute)" + }, { "name": "gcpProject", "type": "String", diff --git a/ui/src/config/zts.json b/ui/src/config/zts.json index 89ae87d6eab..24ad4d64eb9 100644 --- a/ui/src/config/zts.json +++ b/ui/src/config/zts.json @@ -2004,6 +2004,18 @@ "optional": true, "comment": "associated azure subscription id" }, + { + "name": "azureTenant", + "type": "String", + "optional": true, + "comment": "associated azure tenant id" + }, + { + "name": "azureClient", + "type": "String", + "optional": true, + "comment": "associated azure client id" + }, { "name": "gcpProjectId", "type": "String",