From 354ce459f7aad7cf43c3c0dfd840c7906f2a850d Mon Sep 17 00:00:00 2001 From: Henry Avetisyan Date: Wed, 10 Jul 2024 10:11:54 -0700 Subject: [PATCH] add x509-cert-signer-keyid and ssh-cert-signer-keyid fields to domain meta (#2652) Signed-off-by: Henry Avetisyan --- clients/go/zms/model.go | 132 ++ clients/go/zms/zms_schema.go | 2 + .../java/com/yahoo/athenz/zms/Domain.java | 26 + .../java/com/yahoo/athenz/zms/DomainData.java | 26 + .../java/com/yahoo/athenz/zms/DomainMeta.java | 26 + .../java/com/yahoo/athenz/zms/SubDomain.java | 26 + .../com/yahoo/athenz/zms/TopLevelDomain.java | 26 + .../java/com/yahoo/athenz/zms/UserDomain.java | 26 + .../java/com/yahoo/athenz/zms/ZMSSchema.java | 4 +- core/zms/src/main/rdl/Domain.tdl | 2 + .../java/com/yahoo/athenz/zms/DomainTest.java | 110 +- .../com/yahoo/athenz/zms/ZMSCoreTest.java | 22 +- libs/go/zmscli/cli.go | 30 + libs/go/zmscli/domain.go | 34 + .../zms/schema/updates/update-20240708.sql | 2 + servers/zms/schema/zms_server.mwb | Bin 54528 -> 54243 bytes servers/zms/schema/zms_server.sql | 4 +- .../java/com/yahoo/athenz/zms/DBService.java | 16 +- .../java/com/yahoo/athenz/zms/ZMSConsts.java | 5 + .../java/com/yahoo/athenz/zms/ZMSImpl.java | 18 +- .../zms/store/impl/jdbc/JDBCConnection.java | 16 +- .../com/yahoo/athenz/zms/ZMSImplTest.java | 1155 ------------- .../athenz/zms/ZMSMetaAttributeTest.java | 1426 +++++++++++++++++ .../store/impl/jdbc/JDBCConnectionTest.java | 27 +- 24 files changed, 1978 insertions(+), 1183 deletions(-) create mode 100644 servers/zms/schema/updates/update-20240708.sql create mode 100644 servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSMetaAttributeTest.java diff --git a/clients/go/zms/model.go b/clients/go/zms/model.go index 67c26db9997..c0257671673 100644 --- a/clients/go/zms/model.go +++ b/clients/go/zms/model.go @@ -363,6 +363,16 @@ type DomainMeta struct { // ownership information for the domain (read-only attribute) // ResourceOwnership *ResourceDomainOwnership `json:"resourceOwnership,omitempty" rdl:"optional" yaml:",omitempty"` + + // + // requested x509 cert signer key id (system attribute) + // + X509CertSignerKeyId string `json:"x509CertSignerKeyId" rdl:"optional" yaml:",omitempty"` + + // + // requested ssh cert signer key id (system attribute) + // + SshCertSignerKeyId string `json:"sshCertSignerKeyId" rdl:"optional" yaml:",omitempty"` } // NewDomainMeta - creates an initialized DomainMeta instance, returns a pointer to it @@ -495,6 +505,18 @@ func (self *DomainMeta) Validate() error { return fmt.Errorf("DomainMeta.environment does not contain a valid String (%v)", val.Error) } } + if self.X509CertSignerKeyId != "" { + val := rdl.Validate(ZMSSchema(), "String", self.X509CertSignerKeyId) + if !val.Valid { + return fmt.Errorf("DomainMeta.x509CertSignerKeyId does not contain a valid String (%v)", val.Error) + } + } + if self.SshCertSignerKeyId != "" { + val := rdl.Validate(ZMSSchema(), "String", self.SshCertSignerKeyId) + if !val.Valid { + return fmt.Errorf("DomainMeta.sshCertSignerKeyId does not contain a valid String (%v)", val.Error) + } + } return nil } @@ -658,6 +680,16 @@ type Domain struct { // ResourceOwnership *ResourceDomainOwnership `json:"resourceOwnership,omitempty" rdl:"optional" yaml:",omitempty"` + // + // requested x509 cert signer key id (system attribute) + // + X509CertSignerKeyId string `json:"x509CertSignerKeyId" rdl:"optional" yaml:",omitempty"` + + // + // requested ssh cert signer key id (system attribute) + // + SshCertSignerKeyId string `json:"sshCertSignerKeyId" rdl:"optional" yaml:",omitempty"` + // // the common name to be referred to, the symbolic id. It is immutable // @@ -804,6 +836,18 @@ func (self *Domain) Validate() error { return fmt.Errorf("Domain.environment does not contain a valid String (%v)", val.Error) } } + if self.X509CertSignerKeyId != "" { + val := rdl.Validate(ZMSSchema(), "String", self.X509CertSignerKeyId) + if !val.Valid { + return fmt.Errorf("Domain.x509CertSignerKeyId does not contain a valid String (%v)", val.Error) + } + } + if self.SshCertSignerKeyId != "" { + val := rdl.Validate(ZMSSchema(), "String", self.SshCertSignerKeyId) + if !val.Valid { + return fmt.Errorf("Domain.sshCertSignerKeyId does not contain a valid String (%v)", val.Error) + } + } if self.Name == "" { return fmt.Errorf("Domain.name is missing but is a required field") } else { @@ -4103,6 +4147,16 @@ type TopLevelDomain struct { // ResourceOwnership *ResourceDomainOwnership `json:"resourceOwnership,omitempty" rdl:"optional" yaml:",omitempty"` + // + // requested x509 cert signer key id (system attribute) + // + X509CertSignerKeyId string `json:"x509CertSignerKeyId" rdl:"optional" yaml:",omitempty"` + + // + // requested ssh cert signer key id (system attribute) + // + SshCertSignerKeyId string `json:"sshCertSignerKeyId" rdl:"optional" yaml:",omitempty"` + // // name of the domain // @@ -4252,6 +4306,18 @@ func (self *TopLevelDomain) Validate() error { return fmt.Errorf("TopLevelDomain.environment does not contain a valid String (%v)", val.Error) } } + if self.X509CertSignerKeyId != "" { + val := rdl.Validate(ZMSSchema(), "String", self.X509CertSignerKeyId) + if !val.Valid { + return fmt.Errorf("TopLevelDomain.x509CertSignerKeyId does not contain a valid String (%v)", val.Error) + } + } + if self.SshCertSignerKeyId != "" { + val := rdl.Validate(ZMSSchema(), "String", self.SshCertSignerKeyId) + if !val.Valid { + return fmt.Errorf("TopLevelDomain.sshCertSignerKeyId does not contain a valid String (%v)", val.Error) + } + } if self.Name == "" { return fmt.Errorf("TopLevelDomain.name is missing but is a required field") } else { @@ -4421,6 +4487,16 @@ type SubDomain struct { // ResourceOwnership *ResourceDomainOwnership `json:"resourceOwnership,omitempty" rdl:"optional" yaml:",omitempty"` + // + // requested x509 cert signer key id (system attribute) + // + X509CertSignerKeyId string `json:"x509CertSignerKeyId" rdl:"optional" yaml:",omitempty"` + + // + // requested ssh cert signer key id (system attribute) + // + SshCertSignerKeyId string `json:"sshCertSignerKeyId" rdl:"optional" yaml:",omitempty"` + // // name of the domain // @@ -4575,6 +4651,18 @@ func (self *SubDomain) Validate() error { return fmt.Errorf("SubDomain.environment does not contain a valid String (%v)", val.Error) } } + if self.X509CertSignerKeyId != "" { + val := rdl.Validate(ZMSSchema(), "String", self.X509CertSignerKeyId) + if !val.Valid { + return fmt.Errorf("SubDomain.x509CertSignerKeyId does not contain a valid String (%v)", val.Error) + } + } + if self.SshCertSignerKeyId != "" { + val := rdl.Validate(ZMSSchema(), "String", self.SshCertSignerKeyId) + if !val.Valid { + return fmt.Errorf("SubDomain.sshCertSignerKeyId does not contain a valid String (%v)", val.Error) + } + } if self.Name == "" { return fmt.Errorf("SubDomain.name is missing but is a required field") } else { @@ -4753,6 +4841,16 @@ type UserDomain struct { // ResourceOwnership *ResourceDomainOwnership `json:"resourceOwnership,omitempty" rdl:"optional" yaml:",omitempty"` + // + // requested x509 cert signer key id (system attribute) + // + X509CertSignerKeyId string `json:"x509CertSignerKeyId" rdl:"optional" yaml:",omitempty"` + + // + // requested ssh cert signer key id (system attribute) + // + SshCertSignerKeyId string `json:"sshCertSignerKeyId" rdl:"optional" yaml:",omitempty"` + // // user id which will be the domain name // @@ -4894,6 +4992,18 @@ func (self *UserDomain) Validate() error { return fmt.Errorf("UserDomain.environment does not contain a valid String (%v)", val.Error) } } + if self.X509CertSignerKeyId != "" { + val := rdl.Validate(ZMSSchema(), "String", self.X509CertSignerKeyId) + if !val.Valid { + return fmt.Errorf("UserDomain.x509CertSignerKeyId does not contain a valid String (%v)", val.Error) + } + } + if self.SshCertSignerKeyId != "" { + val := rdl.Validate(ZMSSchema(), "String", self.SshCertSignerKeyId) + if !val.Valid { + return fmt.Errorf("UserDomain.sshCertSignerKeyId does not contain a valid String (%v)", val.Error) + } + } if self.Name == "" { return fmt.Errorf("UserDomain.name is missing but is a required field") } else { @@ -7400,6 +7510,16 @@ type DomainData struct { // ResourceOwnership *ResourceDomainOwnership `json:"resourceOwnership,omitempty" rdl:"optional" yaml:",omitempty"` + // + // requested x509 cert signer key id (system attribute) + // + X509CertSignerKeyId string `json:"x509CertSignerKeyId" rdl:"optional" yaml:",omitempty"` + + // + // requested ssh cert signer key id (system attribute) + // + SshCertSignerKeyId string `json:"sshCertSignerKeyId" rdl:"optional" yaml:",omitempty"` + // // name of the domain // @@ -7581,6 +7701,18 @@ func (self *DomainData) Validate() error { return fmt.Errorf("DomainData.environment does not contain a valid String (%v)", val.Error) } } + if self.X509CertSignerKeyId != "" { + val := rdl.Validate(ZMSSchema(), "String", self.X509CertSignerKeyId) + if !val.Valid { + return fmt.Errorf("DomainData.x509CertSignerKeyId does not contain a valid String (%v)", val.Error) + } + } + if self.SshCertSignerKeyId != "" { + val := rdl.Validate(ZMSSchema(), "String", self.SshCertSignerKeyId) + if !val.Valid { + return fmt.Errorf("DomainData.sshCertSignerKeyId does not contain a valid String (%v)", val.Error) + } + } if self.Name == "" { return fmt.Errorf("DomainData.name is missing but is a required field") } else { diff --git a/clients/go/zms/zms_schema.go b/clients/go/zms/zms_schema.go index e106b77834e..7e5aca7ec65 100644 --- a/clients/go/zms/zms_schema.go +++ b/clients/go/zms/zms_schema.go @@ -176,6 +176,8 @@ func init() { tDomainMeta.MapField("contacts", "SimpleName", "String", true, "list of domain contacts (PE-Owner, Product-Owner, etc), each type can have a single value") tDomainMeta.Field("environment", "String", true, nil, "domain environment e.g. production, staging, etc") tDomainMeta.Field("resourceOwnership", "ResourceDomainOwnership", true, nil, "ownership information for the domain (read-only attribute)") + tDomainMeta.Field("x509CertSignerKeyId", "String", true, nil, "requested x509 cert signer key id (system attribute)") + tDomainMeta.Field("sshCertSignerKeyId", "String", true, nil, "requested ssh cert signer key id (system attribute)") sb.AddType(tDomainMeta.Build()) tDomain := rdl.NewStructTypeBuilder("DomainMeta", "Domain") 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 0f9abfc40a2..1da7e6fc7f0 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 @@ -105,6 +105,12 @@ public class Domain { @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) public ResourceDomainOwnership resourceOwnership; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String x509CertSignerKeyId; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String sshCertSignerKeyId; public String name; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -316,6 +322,20 @@ public Domain setResourceOwnership(ResourceDomainOwnership resourceOwnership) { public ResourceDomainOwnership getResourceOwnership() { return resourceOwnership; } + public Domain setX509CertSignerKeyId(String x509CertSignerKeyId) { + this.x509CertSignerKeyId = x509CertSignerKeyId; + return this; + } + public String getX509CertSignerKeyId() { + return x509CertSignerKeyId; + } + public Domain setSshCertSignerKeyId(String sshCertSignerKeyId) { + this.sshCertSignerKeyId = sshCertSignerKeyId; + return this; + } + public String getSshCertSignerKeyId() { + return sshCertSignerKeyId; + } public Domain setName(String name) { this.name = name; return this; @@ -432,6 +452,12 @@ public boolean equals(Object another) { if (resourceOwnership == null ? a.resourceOwnership != null : !resourceOwnership.equals(a.resourceOwnership)) { return false; } + if (x509CertSignerKeyId == null ? a.x509CertSignerKeyId != null : !x509CertSignerKeyId.equals(a.x509CertSignerKeyId)) { + return false; + } + if (sshCertSignerKeyId == null ? a.sshCertSignerKeyId != null : !sshCertSignerKeyId.equals(a.sshCertSignerKeyId)) { + return false; + } if (name == null ? a.name != null : !name.equals(a.name)) { 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 7f8aca4388e..fa6aebbe614 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 @@ -101,6 +101,12 @@ public class DomainData { @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) public ResourceDomainOwnership resourceOwnership; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String x509CertSignerKeyId; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String sshCertSignerKeyId; public String name; public List roles; public SignedPolicies policies; @@ -312,6 +318,20 @@ public DomainData setResourceOwnership(ResourceDomainOwnership resourceOwnership public ResourceDomainOwnership getResourceOwnership() { return resourceOwnership; } + public DomainData setX509CertSignerKeyId(String x509CertSignerKeyId) { + this.x509CertSignerKeyId = x509CertSignerKeyId; + return this; + } + public String getX509CertSignerKeyId() { + return x509CertSignerKeyId; + } + public DomainData setSshCertSignerKeyId(String sshCertSignerKeyId) { + this.sshCertSignerKeyId = sshCertSignerKeyId; + return this; + } + public String getSshCertSignerKeyId() { + return sshCertSignerKeyId; + } public DomainData setName(String name) { this.name = name; return this; @@ -456,6 +476,12 @@ public boolean equals(Object another) { if (resourceOwnership == null ? a.resourceOwnership != null : !resourceOwnership.equals(a.resourceOwnership)) { return false; } + if (x509CertSignerKeyId == null ? a.x509CertSignerKeyId != null : !x509CertSignerKeyId.equals(a.x509CertSignerKeyId)) { + return false; + } + if (sshCertSignerKeyId == null ? a.sshCertSignerKeyId != null : !sshCertSignerKeyId.equals(a.sshCertSignerKeyId)) { + return false; + } if (name == null ? a.name != null : !name.equals(a.name)) { 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 af6e69fc531..5e77b7c4a51 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 @@ -101,6 +101,12 @@ public class DomainMeta { @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) public ResourceDomainOwnership resourceOwnership; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String x509CertSignerKeyId; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String sshCertSignerKeyId; public DomainMeta setDescription(String description) { this.description = description; @@ -305,6 +311,20 @@ public DomainMeta setResourceOwnership(ResourceDomainOwnership resourceOwnership public ResourceDomainOwnership getResourceOwnership() { return resourceOwnership; } + public DomainMeta setX509CertSignerKeyId(String x509CertSignerKeyId) { + this.x509CertSignerKeyId = x509CertSignerKeyId; + return this; + } + public String getX509CertSignerKeyId() { + return x509CertSignerKeyId; + } + public DomainMeta setSshCertSignerKeyId(String sshCertSignerKeyId) { + this.sshCertSignerKeyId = sshCertSignerKeyId; + return this; + } + public String getSshCertSignerKeyId() { + return sshCertSignerKeyId; + } @Override public boolean equals(Object another) { @@ -400,6 +420,12 @@ public boolean equals(Object another) { if (resourceOwnership == null ? a.resourceOwnership != null : !resourceOwnership.equals(a.resourceOwnership)) { return false; } + if (x509CertSignerKeyId == null ? a.x509CertSignerKeyId != null : !x509CertSignerKeyId.equals(a.x509CertSignerKeyId)) { + return false; + } + if (sshCertSignerKeyId == null ? a.sshCertSignerKeyId != null : !sshCertSignerKeyId.equals(a.sshCertSignerKeyId)) { + return false; + } } return true; } 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 aba61aecbaf..69c785fab49 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 @@ -101,6 +101,12 @@ public class SubDomain { @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) public ResourceDomainOwnership resourceOwnership; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String x509CertSignerKeyId; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String sshCertSignerKeyId; public String name; public List adminUsers; @RdlOptional @@ -311,6 +317,20 @@ public SubDomain setResourceOwnership(ResourceDomainOwnership resourceOwnership) public ResourceDomainOwnership getResourceOwnership() { return resourceOwnership; } + public SubDomain setX509CertSignerKeyId(String x509CertSignerKeyId) { + this.x509CertSignerKeyId = x509CertSignerKeyId; + return this; + } + public String getX509CertSignerKeyId() { + return x509CertSignerKeyId; + } + public SubDomain setSshCertSignerKeyId(String sshCertSignerKeyId) { + this.sshCertSignerKeyId = sshCertSignerKeyId; + return this; + } + public String getSshCertSignerKeyId() { + return sshCertSignerKeyId; + } public SubDomain setName(String name) { this.name = name; return this; @@ -434,6 +454,12 @@ public boolean equals(Object another) { if (resourceOwnership == null ? a.resourceOwnership != null : !resourceOwnership.equals(a.resourceOwnership)) { return false; } + if (x509CertSignerKeyId == null ? a.x509CertSignerKeyId != null : !x509CertSignerKeyId.equals(a.x509CertSignerKeyId)) { + return false; + } + if (sshCertSignerKeyId == null ? a.sshCertSignerKeyId != null : !sshCertSignerKeyId.equals(a.sshCertSignerKeyId)) { + return false; + } if (name == null ? a.name != null : !name.equals(a.name)) { 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 71d8284c5e3..68233a4bbc7 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 @@ -102,6 +102,12 @@ public class TopLevelDomain { @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) public ResourceDomainOwnership resourceOwnership; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String x509CertSignerKeyId; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String sshCertSignerKeyId; public String name; public List adminUsers; @RdlOptional @@ -311,6 +317,20 @@ public TopLevelDomain setResourceOwnership(ResourceDomainOwnership resourceOwner public ResourceDomainOwnership getResourceOwnership() { return resourceOwnership; } + public TopLevelDomain setX509CertSignerKeyId(String x509CertSignerKeyId) { + this.x509CertSignerKeyId = x509CertSignerKeyId; + return this; + } + public String getX509CertSignerKeyId() { + return x509CertSignerKeyId; + } + public TopLevelDomain setSshCertSignerKeyId(String sshCertSignerKeyId) { + this.sshCertSignerKeyId = sshCertSignerKeyId; + return this; + } + public String getSshCertSignerKeyId() { + return sshCertSignerKeyId; + } public TopLevelDomain setName(String name) { this.name = name; return this; @@ -427,6 +447,12 @@ public boolean equals(Object another) { if (resourceOwnership == null ? a.resourceOwnership != null : !resourceOwnership.equals(a.resourceOwnership)) { return false; } + if (x509CertSignerKeyId == null ? a.x509CertSignerKeyId != null : !x509CertSignerKeyId.equals(a.x509CertSignerKeyId)) { + return false; + } + if (sshCertSignerKeyId == null ? a.sshCertSignerKeyId != null : !sshCertSignerKeyId.equals(a.sshCertSignerKeyId)) { + return false; + } if (name == null ? a.name != null : !name.equals(a.name)) { 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 3f1190dc8b8..636395c9aec 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 @@ -101,6 +101,12 @@ public class UserDomain { @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) public ResourceDomainOwnership resourceOwnership; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String x509CertSignerKeyId; + @RdlOptional + @JsonInclude(JsonInclude.Include.NON_NULL) + public String sshCertSignerKeyId; public String name; @RdlOptional @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -309,6 +315,20 @@ public UserDomain setResourceOwnership(ResourceDomainOwnership resourceOwnership public ResourceDomainOwnership getResourceOwnership() { return resourceOwnership; } + public UserDomain setX509CertSignerKeyId(String x509CertSignerKeyId) { + this.x509CertSignerKeyId = x509CertSignerKeyId; + return this; + } + public String getX509CertSignerKeyId() { + return x509CertSignerKeyId; + } + public UserDomain setSshCertSignerKeyId(String sshCertSignerKeyId) { + this.sshCertSignerKeyId = sshCertSignerKeyId; + return this; + } + public String getSshCertSignerKeyId() { + return sshCertSignerKeyId; + } public UserDomain setName(String name) { this.name = name; return this; @@ -418,6 +438,12 @@ public boolean equals(Object another) { if (resourceOwnership == null ? a.resourceOwnership != null : !resourceOwnership.equals(a.resourceOwnership)) { return false; } + if (x509CertSignerKeyId == null ? a.x509CertSignerKeyId != null : !x509CertSignerKeyId.equals(a.x509CertSignerKeyId)) { + return false; + } + if (sshCertSignerKeyId == null ? a.sshCertSignerKeyId != null : !sshCertSignerKeyId.equals(a.sshCertSignerKeyId)) { + return false; + } if (name == null ? a.name != null : !name.equals(a.name)) { 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 4cdf030b98d..9e69804bc86 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 @@ -150,7 +150,9 @@ private static Schema build() { .field("featureFlags", "Int32", true, "features enabled per domain (system attribute)") .mapField("contacts", "SimpleName", "String", true, "list of domain contacts (PE-Owner, Product-Owner, etc), each type can have a single value") .field("environment", "String", true, "domain environment e.g. production, staging, etc") - .field("resourceOwnership", "ResourceDomainOwnership", true, "ownership information for the domain (read-only attribute)"); + .field("resourceOwnership", "ResourceDomainOwnership", true, "ownership information for the domain (read-only attribute)") + .field("x509CertSignerKeyId", "String", true, "requested x509 cert signer key id (system attribute)") + .field("sshCertSignerKeyId", "String", true, "requested ssh cert signer key id (system attribute)"); sb.structType("Domain", "DomainMeta") .comment("A domain is an independent partition of users, roles, and resources. Its name represents the definition of a namespace; the only way a new namespace can be created, from the top, is by creating Domains. Administration of a domain is governed by the parent domain (using reverse-DNS namespaces). The top level domains are governed by the special \"sys.auth\" domain.") diff --git a/core/zms/src/main/rdl/Domain.tdl b/core/zms/src/main/rdl/Domain.tdl index 32170d6772d..eb3ea4574c2 100644 --- a/core/zms/src/main/rdl/Domain.tdl +++ b/core/zms/src/main/rdl/Domain.tdl @@ -41,6 +41,8 @@ type DomainMeta Struct { Map contacts (optional); //list of domain contacts (PE-Owner, Product-Owner, etc), each type can have a single value String environment (optional, x_allowempty="true"); //domain environment e.g. production, staging, etc ResourceDomainOwnership resourceOwnership (optional); //ownership information for the domain (read-only attribute) + String x509CertSignerKeyId (optional, x_allowempty="true"); //requested x509 cert signer key id (system attribute) + String sshCertSignerKeyId (optional, x_allowempty="true"); //requested ssh cert signer key id (system attribute) } //A domain is an independent partition of users, roles, and resources. 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 f7141a2e190..5aedf6518dd 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 @@ -87,7 +87,8 @@ public void testDomainMetaMethod() { .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") - .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")) + .setX509CertSignerKeyId("x509-keyid").setSshCertSignerKeyId("ssh-keyid"); Validator.Result result = validator.validate(dm, "DomainMeta"); assertTrue(result.valid); @@ -121,6 +122,8 @@ public void testDomainMetaMethod() { assertEquals(dm.getFeatureFlags(), 3); assertEquals(dm.getContacts(), Map.of("pe-owner", "user.test")); assertEquals(dm.getEnvironment(), "production"); + assertEquals(dm.getX509CertSignerKeyId(), "x509-keyid"); + assertEquals(dm.getSshCertSignerKeyId(), "ssh-keyid"); assertEquals(dm.getResourceOwnership(), new ResourceDomainOwnership().setMetaOwner("TF")); DomainMeta dm2 = new DomainMeta().init(); @@ -134,11 +137,26 @@ public void testDomainMetaMethod() { .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") - .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")) + .setX509CertSignerKeyId("x509-keyid").setSshCertSignerKeyId("ssh-keyid"); assertEquals(dm, dm2); assertEquals(dm, dm); + dm2.setX509CertSignerKeyId("x509-keyid2"); + assertNotEquals(dm, dm2); + dm2.setX509CertSignerKeyId(null); + assertNotEquals(dm, dm2); + dm2.setX509CertSignerKeyId("x509-keyid"); + assertEquals(dm, dm2); + + dm2.setSshCertSignerKeyId("ssh-keyid2"); + assertNotEquals(dm, dm2); + dm2.setSshCertSignerKeyId(null); + assertNotEquals(dm, dm2); + dm2.setSshCertSignerKeyId("ssh-keyid"); + assertEquals(dm, dm2); + dm2.setEnvironment("staging"); assertNotEquals(dm, dm2); dm2.setEnvironment(null); @@ -350,7 +368,8 @@ public void testTopLevelDomainMethod() { .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProject("gcp") .setGcpProjectNumber("1242").setProductId("abcd-1234").setFeatureFlags(3) .setContacts(Map.of("pe-owner", "user.test")).setEnvironment("production") - .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")) + .setX509CertSignerKeyId("x509-keyid").setSshCertSignerKeyId("ssh-keyid"); result = validator.validate(tld, "TopLevelDomain"); assertTrue(result.valid); @@ -387,6 +406,8 @@ public void testTopLevelDomainMethod() { assertEquals(tld.getFeatureFlags(), 3); assertEquals(tld.getContacts(), Map.of("pe-owner", "user.test")); assertEquals(tld.getEnvironment(), "production"); + assertEquals(tld.getX509CertSignerKeyId(), "x509-keyid"); + assertEquals(tld.getSshCertSignerKeyId(), "ssh-keyid"); assertEquals(tld.getResourceOwnership(), new ResourceDomainOwnership().setMetaOwner("TF")); TopLevelDomain tld2 = new TopLevelDomain().setDescription("domain desc").setOrg("org:test").setEnabled(true) @@ -399,7 +420,8 @@ public void testTopLevelDomainMethod() { .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProject("gcp") .setGcpProjectNumber("1242").setProductId("abcd-1234").setFeatureFlags(3) .setContacts(Map.of("pe-owner", "user.test")).setEnvironment("production") - .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")) + .setX509CertSignerKeyId("x509-keyid").setSshCertSignerKeyId("ssh-keyid"); assertEquals(tld, tld2); assertEquals(tld, tld); @@ -411,6 +433,20 @@ public void testTopLevelDomainMethod() { tld2.setEnvironment("production"); assertEquals(tld, tld2); + tld2.setX509CertSignerKeyId("x509-keyid2"); + assertNotEquals(tld, tld2); + tld2.setX509CertSignerKeyId(null); + assertNotEquals(tld, tld2); + tld2.setX509CertSignerKeyId("x509-keyid"); + assertEquals(tld, tld2); + + tld2.setSshCertSignerKeyId("ssh-keyid2"); + assertNotEquals(tld, tld2); + tld2.setSshCertSignerKeyId(null); + assertNotEquals(tld, tld2); + tld2.setSshCertSignerKeyId("ssh-keyid"); + assertEquals(tld, tld2); + tld2.setContacts(Map.of("product-owner", "user.test")); assertNotEquals(tld, tld2); tld2.setContacts(null); @@ -603,7 +639,8 @@ public void testSubDomainMethod() { .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProject("gcp") .setGcpProjectNumber("1244").setProductId("abcd-1234").setFeatureFlags(3) .setContacts(Map.of("pe-owner", "user.test")).setEnvironment("production") - .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")) + .setX509CertSignerKeyId("x509-keyid").setSshCertSignerKeyId("ssh-keyid"); Validator.Result result = validator.validate(sd, "SubDomain"); assertTrue(result.valid, result.error); @@ -641,6 +678,8 @@ public void testSubDomainMethod() { assertEquals(sd.getFeatureFlags(), 3); assertEquals(sd.getContacts(), Map.of("pe-owner", "user.test")); assertEquals(sd.getEnvironment(), "production"); + assertEquals(sd.getX509CertSignerKeyId(), "x509-keyid"); + assertEquals(sd.getSshCertSignerKeyId(), "ssh-keyid"); assertEquals(sd.getResourceOwnership(), new ResourceDomainOwnership().setMetaOwner("TF")); SubDomain sd2 = new SubDomain().setDescription("domain desc").setOrg("org:test").setEnabled(true) @@ -655,7 +694,8 @@ public void testSubDomainMethod() { .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProject("gcp") .setGcpProjectNumber("1244").setProductId("abcd-1234").setFeatureFlags(3) .setContacts(Map.of("pe-owner", "user.test")).setEnvironment("production") - .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")) + .setX509CertSignerKeyId("x509-keyid").setSshCertSignerKeyId("ssh-keyid"); assertEquals(sd, sd2); assertEquals(sd, sd); @@ -668,6 +708,20 @@ public void testSubDomainMethod() { sd2.setEnvironment("production"); assertEquals(sd, sd2); + sd2.setX509CertSignerKeyId("x509-keyid2"); + assertNotEquals(sd, sd2); + sd2.setX509CertSignerKeyId(null); + assertNotEquals(sd, sd2); + sd2.setX509CertSignerKeyId("x509-keyid"); + assertEquals(sd, sd2); + + sd2.setSshCertSignerKeyId("ssh-keyid2"); + assertNotEquals(sd, sd2); + sd2.setSshCertSignerKeyId(null); + assertNotEquals(sd, sd2); + sd2.setSshCertSignerKeyId("ssh-keyid"); + assertEquals(sd, sd2); + sd2.setContacts(Map.of("product-owner", "user.test")); assertNotEquals(sd, sd2); sd2.setContacts(null); @@ -858,7 +912,8 @@ public void testUserDomainMethod() { .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")) - .setEnvironment("production").setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")); + .setEnvironment("production").setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")) + .setX509CertSignerKeyId("x509-keyid").setSshCertSignerKeyId("ssh-keyid"); Validator.Result result = validator.validate(ud, "UserDomain"); assertTrue(result.valid); @@ -894,6 +949,8 @@ public void testUserDomainMethod() { assertEquals(ud.getFeatureFlags(), 3); assertEquals(ud.getContacts(), Map.of("pe-owner", "user.test")); assertEquals(ud.getEnvironment(), "production"); + assertEquals(ud.getX509CertSignerKeyId(), "x509-keyid"); + assertEquals(ud.getSshCertSignerKeyId(), "ssh-keyid"); assertEquals(ud.getResourceOwnership(), new ResourceDomainOwnership().setMetaOwner("TF")); UserDomain ud2 = new UserDomain().setDescription("domain desc").setOrg("org:test").setEnabled(true) @@ -907,7 +964,8 @@ public void testUserDomainMethod() { .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")) - .setEnvironment("production").setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")); + .setEnvironment("production").setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")) + .setX509CertSignerKeyId("x509-keyid").setSshCertSignerKeyId("ssh-keyid"); assertEquals(ud, ud2); assertEquals(ud, ud); @@ -919,6 +977,20 @@ public void testUserDomainMethod() { ud2.setEnvironment("production"); assertEquals(ud, ud2); + ud2.setX509CertSignerKeyId("x509-keyid2"); + assertNotEquals(ud, ud2); + ud2.setX509CertSignerKeyId(null); + assertNotEquals(ud, ud2); + ud2.setX509CertSignerKeyId("x509-keyid"); + assertEquals(ud, ud2); + + ud2.setSshCertSignerKeyId("ssh-keyid2"); + assertNotEquals(ud, ud2); + ud2.setSshCertSignerKeyId(null); + assertNotEquals(ud, ud2); + ud2.setSshCertSignerKeyId("ssh-keyid"); + assertEquals(ud, ud2); + ud2.setContacts(Map.of("product-owner", "user.test")); assertNotEquals(ud, ud2); ud2.setContacts(null); @@ -1129,7 +1201,8 @@ public void testDomainMethod() { .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProject("gcp") .setGcpProjectNumber("1237").setProductId("abcd-1234").setFeatureFlags(3) .setContacts(Map.of("pe-owner", "user.test")).setEnvironment("production") - .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")) + .setX509CertSignerKeyId("x509-keyid").setSshCertSignerKeyId("ssh-keyid"); Validator.Result result = validator.validate(d, "Domain"); assertTrue(result.valid); @@ -1166,6 +1239,8 @@ public void testDomainMethod() { assertEquals(d.getFeatureFlags(), 3); assertEquals(d.getContacts(), Map.of("pe-owner", "user.test")); assertEquals(d.getEnvironment(), "production"); + assertEquals(d.getX509CertSignerKeyId(), "x509-keyid"); + assertEquals(d.getSshCertSignerKeyId(), "ssh-keyid"); assertEquals(d.getResourceOwnership(), new ResourceDomainOwnership().setMetaOwner("TF")); Domain d2 = new Domain(); @@ -1180,7 +1255,8 @@ public void testDomainMethod() { .setBusinessService("business-service").setMemberPurgeExpiryDays(10).setGcpProject("gcp") .setGcpProjectNumber("1237").setProductId("abcd-1234").setFeatureFlags(3) .setContacts(Map.of("pe-owner", "user.test")).setEnvironment("production") - .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")); + .setResourceOwnership(new ResourceDomainOwnership().setMetaOwner("TF")) + .setX509CertSignerKeyId("x509-keyid").setSshCertSignerKeyId("ssh-keyid"); assertEquals(d, d2); assertEquals(d, d); @@ -1192,6 +1268,20 @@ public void testDomainMethod() { d2.setEnvironment("production"); assertEquals(d, d2); + d2.setX509CertSignerKeyId("x509-keyid2"); + assertNotEquals(d, d2); + d2.setX509CertSignerKeyId(null); + assertNotEquals(d, d2); + d2.setX509CertSignerKeyId("x509-keyid"); + assertEquals(d, d2); + + d2.setSshCertSignerKeyId("ssh-keyid2"); + assertNotEquals(d, d2); + d2.setSshCertSignerKeyId(null); + assertNotEquals(d, d2); + d2.setSshCertSignerKeyId("ssh-keyid"); + assertEquals(d, d2); + d2.setContacts(Map.of("product-owner", "user.test")); assertNotEquals(d, d2); d2.setContacts(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 f93d1e0e547..e95443cfc94 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 @@ -590,7 +590,8 @@ public void testSignedDomainsMethod() { .setTags(Collections.singletonMap("tagKey", new TagValueList().setList(Collections.singletonList("tagValue")))) .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")); + .setEnvironment("production").setResourceOwnership(new ResourceDomainOwnership().setObjectOwner("TF")) + .setX509CertSignerKeyId("x509-keyid").setSshCertSignerKeyId("ssh-keyid"); result = validator.validate(dd, "DomainData"); assertTrue(result.valid, result.error); @@ -631,6 +632,8 @@ public void testSignedDomainsMethod() { assertEquals(dd.getFeatureFlags(), 3); assertEquals(dd.getContacts(), Map.of("pe-owner", "user.test")); assertEquals(dd.getEnvironment(), "production"); + assertEquals(dd.getX509CertSignerKeyId(), "x509-keyid"); + assertEquals(dd.getSshCertSignerKeyId(), "ssh-keyid"); assertEquals(dd.getResourceOwnership(), new ResourceDomainOwnership().setObjectOwner("TF")); DomainData dd2 = new DomainData().setName("test.domain").setAccount("aws").setYpmId(1).setRoles(rl) @@ -644,7 +647,8 @@ public void testSignedDomainsMethod() { .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")); + .setResourceOwnership(new ResourceDomainOwnership().setObjectOwner("TF")) + .setX509CertSignerKeyId("x509-keyid").setSshCertSignerKeyId("ssh-keyid"); assertEquals(dd2, dd); assertNotEquals(dd, null); @@ -666,6 +670,20 @@ public void testSignedDomainsMethod() { dd2.setEnvironment("production"); assertEquals(dd, dd2); + dd2.setX509CertSignerKeyId("x509-keyid2"); + assertNotEquals(dd, dd2); + dd2.setX509CertSignerKeyId(null); + assertNotEquals(dd, dd2); + dd2.setX509CertSignerKeyId("x509-keyid"); + assertEquals(dd, dd2); + + dd2.setSshCertSignerKeyId("ssh-keyid2"); + assertNotEquals(dd, dd2); + dd2.setSshCertSignerKeyId(null); + assertNotEquals(dd, dd2); + dd2.setSshCertSignerKeyId("ssh-keyid"); + assertEquals(dd, dd2); + dd2.setContacts(Map.of("product-owner", "user.test")); assertNotEquals(dd, dd2); dd2.setContacts(null); diff --git a/libs/go/zmscli/cli.go b/libs/go/zmscli/cli.go index 330ddf16eb3..cf15d47603e 100644 --- a/libs/go/zmscli/cli.go +++ b/libs/go/zmscli/cli.go @@ -951,6 +951,14 @@ func (cli Zms) EvalCommand(params []string) (*string, error) { if argc == 1 { return cli.SetDomainUserAuthorityFilter(dn, args[0]) } + case "set-domain-x509-cert-signer-keyid": + if argc == 1 { + return cli.SetDomainX509CertSignerKeyId(dn, args[0]) + } + case "set-domain-ssh-cert-signer-keyid": + if argc == 1 { + return cli.SetDomainSshCertSignerKeyId(dn, args[0]) + } case "set-domain-environment": if argc == 1 { return cli.SetDomainEnvironment(dn, args[0]) @@ -1585,6 +1593,26 @@ func (cli Zms) HelpSpecificCommand(interactive bool, cmd string) string { buf.WriteString(" filter : comma separated list of user authority filters\n") buf.WriteString(" examples:\n") buf.WriteString(" " + domainExample + " set-domain-user-authority-filter OnShore-US\n") + case "set-domain-x509-cert-signer-keyid": + buf.WriteString(" syntax:\n") + buf.WriteString(" [-o json] " + domainParam + " set-domain-x509-cert-signer-keyid key-id\n") + buf.WriteString(" parameters:\n") + if !interactive { + buf.WriteString(" domain : name of the domain being updated\n") + } + buf.WriteString(" key-id : certificate signer key id\n") + buf.WriteString(" examples:\n") + buf.WriteString(" " + domainExample + " set-domain-x509-cert-signer-keyid keyid1\n") + case "set-domain-ssh-cert-signer-keyid": + buf.WriteString(" syntax:\n") + buf.WriteString(" [-o json] " + domainParam + " set-domain-ssh-cert-signer-keyid key-id\n") + buf.WriteString(" parameters:\n") + if !interactive { + buf.WriteString(" domain : name of the domain being updated\n") + } + buf.WriteString(" key-id : certificate signer key id\n") + buf.WriteString(" examples:\n") + buf.WriteString(" " + domainExample + " set-domain-ssh-cert-signer-keyid keyid1\n") case "set-domain-environment": buf.WriteString(" syntax:\n") buf.WriteString(" [-o json] " + domainParam + " set-domain-environment environment\n") @@ -3571,6 +3599,8 @@ func (cli Zms) HelpListCommand() string { buf.WriteString(" set-domain-role-cert-expiry-mins cert-expiry-mins\n") buf.WriteString(" set-domain-token-sign-algorithm algorithm\n") buf.WriteString(" set-domain-user-authority-filter filter\n") + buf.WriteString(" set-domain-x509-cert-signer-keyid key-id\n") + buf.WriteString(" set-domain-ssh-cert-signer-keyid key-id\n") buf.WriteString(" set-domain-environment environment\n") buf.WriteString(" set-domain-feature-flags flags\n") buf.WriteString(" set-domain-contact type user\n") diff --git a/libs/go/zmscli/domain.go b/libs/go/zmscli/domain.go index 65f2763b440..a857de39d95 100644 --- a/libs/go/zmscli/domain.go +++ b/libs/go/zmscli/domain.go @@ -770,6 +770,40 @@ func (cli Zms) SetDomainAuditEnabled(dn string, auditEnabled bool) (*string, err return cli.dumpByFormat(message, cli.buildYAMLOutput) } +func (cli Zms) SetDomainX509CertSignerKeyId(dn, keyId string) (*string, error) { + meta := zms.DomainMeta{ + X509CertSignerKeyId: keyId, + } + err := cli.Zms.PutDomainSystemMeta(zms.DomainName(dn), "x509certsignerkeyid", cli.AuditRef, &meta) + if err != nil { + return nil, err + } + s := "[domain " + dn + " metadata successfully updated]\n" + message := SuccessMessage{ + Status: 200, + Message: s, + } + + return cli.dumpByFormat(message, cli.buildYAMLOutput) +} + +func (cli Zms) SetDomainSshCertSignerKeyId(dn, keyId string) (*string, error) { + meta := zms.DomainMeta{ + SshCertSignerKeyId: keyId, + } + err := cli.Zms.PutDomainSystemMeta(zms.DomainName(dn), "sshcertsignerkeyid", cli.AuditRef, &meta) + if err != nil { + return nil, err + } + s := "[domain " + dn + " metadata successfully updated]\n" + message := SuccessMessage{ + Status: 200, + Message: s, + } + + return cli.dumpByFormat(message, cli.buildYAMLOutput) +} + func (cli Zms) SetDomainUserAuthorityFilter(dn, filter string) (*string, error) { meta := zms.DomainMeta{ UserAuthorityFilter: filter, diff --git a/servers/zms/schema/updates/update-20240708.sql b/servers/zms/schema/updates/update-20240708.sql new file mode 100644 index 00000000000..f3cd8374c69 --- /dev/null +++ b/servers/zms/schema/updates/update-20240708.sql @@ -0,0 +1,2 @@ +ALTER TABLE `zms_server`.`domain` ADD `x509_cert_signer_keyid` VARCHAR(64) NOT NULL DEFAULT ''; +ALTER TABLE `zms_server`.`domain` ADD `ssh_cert_signer_keyid` VARCHAR(64) NOT NULL DEFAULT ''; diff --git a/servers/zms/schema/zms_server.mwb b/servers/zms/schema/zms_server.mwb index 14ca71ead9e1504baa358a1fd190829f288107ac..8add9312cd82282c37b5ea1c9c531e70f825425f 100644 GIT binary patch literal 54243 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 Fe*-<$dmaD) literal 54528 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 z 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 z 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 domMod1); - zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); - } - - @Test - public void testPutDomainMetaInvalid() { - - // enable product id support - - System.setProperty(ZMSConsts.ZMS_PROP_PRODUCT_ID_SUPPORT, "true"); - ZMSImpl zmsImpl = zmsTestInitializer.zmsInit(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - final String auditRef = zmsTestInitializer.getAuditRef(); - - final String domainName = "MetaDomProductid"; - TopLevelDomain dom = zmsTestInitializer.createTopLevelDomainObject(domainName, - "Test Domain", "testOrg", zmsTestInitializer.getAdminUser()); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom); - - Domain resDom = zmsImpl.getDomain(ctx, domainName); - assertNotNull(resDom); - assertEquals(resDom.getDescription(), "Test Domain"); - assertEquals(resDom.getOrg(), "testorg"); - assertTrue(resDom.getEnabled()); - assertFalse(resDom.getAuditEnabled()); - Integer productId = resDom.getYpmId(); - - zmsTestInitializer.setupPrincipalSystemMetaDelete(zmsImpl, ctx.principal().getFullName(), - domainName, "domain", "productid"); - DomainMeta meta = zmsTestInitializer.createDomainMetaObject("Test2 Domain", "NewOrg", - true, true, "12345", null); - try { - zmsImpl.putDomainSystemMeta(ctx, domainName, "productid", auditRef, meta); - fail("bad request exc not thrown"); - } catch (ResourceException exc) { - assertEquals(400, exc.getCode()); - assertTrue(exc.getMessage().contains("Unique Product Id must be specified for top level domain")); - } - - // put metadata using another domains productId - dom = zmsTestInitializer.createTopLevelDomainObject("MetaDomProductid2", - "Test Domain", "testOrg", zmsTestInitializer.getAdminUser()); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom); - - resDom = zmsImpl.getDomain(ctx, "MetaDomProductid2"); - Integer productId2 = resDom.getYpmId(); - assertNotEquals(productId, productId2); - - meta = zmsTestInitializer.createDomainMetaObject("Test3 Domain", "NewOrg", - true, true, "12345", productId2); - try { - zmsImpl.putDomainSystemMeta(ctx, domainName, "productid", auditRef, meta); - fail("bad request exc not thrown"); - } catch (ResourceException exc) { - assertEquals(400, exc.getCode()); - assertTrue(exc.getMessage().contains("is already assigned to domain")); - } - - // test negative values - - meta = new DomainMeta().setServiceExpiryDays(-10); - try { - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); - fail(); - } catch (ResourceException ex) { - assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); - } - - meta = new DomainMeta().setGroupExpiryDays(-10); - try { - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); - fail(); - } catch (ResourceException ex) { - assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); - } - - meta = new DomainMeta().setMemberExpiryDays(-10); - try { - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); - fail(); - } catch (ResourceException ex) { - assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); - } - - meta = new DomainMeta().setRoleCertExpiryMins(-10); - try { - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); - fail(); - } catch (ResourceException ex) { - assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); - } - - meta = new DomainMeta().setServiceCertExpiryMins(-10); - try { - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); - fail(); - } catch (ResourceException ex) { - assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); - } - - meta = new DomainMeta().setTokenExpiryMins(-10); - try { - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); - fail(); - } catch (ResourceException ex) { - assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); - } - - zmsTestInitializer.cleanupPrincipalSystemMetaDelete(zmsImpl, "domain"); - zmsImpl.deleteTopLevelDomain(ctx, "MetaDomProductid", auditRef, null); - zmsImpl.deleteTopLevelDomain(ctx, "MetaDomProductid2", auditRef, null); - System.clearProperty(ZMSConsts.ZMS_PROP_PRODUCT_ID_SUPPORT); - zmsImpl.objectStore.clearConnections(); - } - - @Test - public void testPutDomainMetaDefaults() { - - final String domainName = "meta-dom-values"; - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - final String auditRef = zmsTestInitializer.getAuditRef(); - - TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, null, null, - zmsTestInitializer.getAdminUser()); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); - - Domain resDom1 = zmsImpl.getDomain(ctx, domainName); - assertNotNull(resDom1); - assertNull(resDom1.getDescription()); - assertNull(resDom1.getOrg()); - assertTrue(resDom1.getEnabled()); - assertFalse(resDom1.getAuditEnabled()); - - DomainMeta meta = zmsTestInitializer.createDomainMetaObject("Test2 Domain", "NewOrg", true, false, null, 0); - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); - - zmsImpl.putDomainSystemMeta(ctx, domainName, "org", auditRef, meta); - - Domain resDom3 = zmsImpl.getDomain(ctx, domainName); - assertNotNull(resDom3); - assertEquals(resDom3.getDescription(), "Test2 Domain"); - assertEquals(resDom3.getOrg(), "neworg"); - assertTrue(resDom3.getEnabled()); - assertFalse(resDom3.getAuditEnabled()); - assertNull(resDom3.getAccount()); - assertNull(resDom3.getAzureSubscription()); - assertNull(resDom3.getGcpProject()); - assertNull(resDom3.getBusinessService()); - assertEquals(Integer.valueOf(0), resDom3.getYpmId()); - - meta.setAccount("aws"); - zmsImpl.putDomainSystemMeta(ctx, domainName, "account", auditRef, meta); - resDom3 = zmsImpl.getDomain(ctx, domainName); - assertNotNull(resDom3); - assertEquals(resDom3.getOrg(), "neworg"); - assertEquals(resDom3.getAccount(), "aws"); - assertNull(resDom3.getAzureSubscription()); - assertNull(resDom3.getGcpProject()); - 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()); - - meta.setGcpProject("gcp"); - meta.setGcpProjectNumber("1239"); - zmsImpl.putDomainSystemMeta(ctx, domainName, "gcpproject", 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"); - assertEquals(resDom3.getGcpProject(), "gcp"); - assertEquals(resDom3.getGcpProjectNumber(), "1239"); - assertNull(resDom3.getBusinessService()); - - meta.setBusinessService("123:business service"); - zmsImpl.putDomainSystemMeta(ctx, domainName, "businessservice", 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"); - assertEquals(resDom3.getGcpProject(), "gcp"); - assertEquals(resDom3.getGcpProjectNumber(), "1239"); - assertEquals(resDom3.getBusinessService(), "123:business service"); - - zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); - } - - @Test - public void testPutDomainMetaMissingAuditRef() { - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - final String auditRef = zmsTestInitializer.getAuditRef(); - - String domain = "testSetDomainMetaMissingAuditRef"; - TopLevelDomain dom = zmsTestInitializer.createTopLevelDomainObject( - domain, "Test1 Domain", "testOrg", zmsTestInitializer.getAdminUser()); - dom.setAuditEnabled(true); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom); - - Domain resDom = zmsImpl.getDomain(ctx, domain); - assertNotNull(resDom); - assertEquals(resDom.getDescription(), "Test1 Domain"); - assertEquals(resDom.getOrg(), "testorg"); - assertTrue(resDom.getAuditEnabled()); - - DomainMeta meta = zmsTestInitializer.createDomainMetaObject("Test2 Domain", "NewOrg", false, true, null, 0); - try { - zmsImpl.putDomainMeta(ctx, domain, null, null, meta); - fail(); - } catch (ResourceException ex) { - assertEquals(ex.getCode(), 400); - assertTrue(ex.getMessage().contains("Audit reference required")); - } finally { - zmsImpl.deleteTopLevelDomain(ctx, domain, auditRef, null); - } - } - - @Test - public void testPutDomainMetaSubDomain() { - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - final String auditRef = zmsTestInitializer.getAuditRef(); - - try { - TopLevelDomain dom = zmsTestInitializer.createTopLevelDomainObject("MetaDomProductid", - "Test Domain", "testOrg", zmsTestInitializer.getAdminUser(), ctx.principal().getFullName()); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom); - } catch (ResourceException rexc) { - assertEquals(400, rexc.getCode()); - } - - SubDomain subDom = zmsTestInitializer.createSubDomainObject("metaSubDom", "MetaDomProductid", - "sub Domain", "testOrg", zmsTestInitializer.getAdminUser()); - zmsImpl.postSubDomain(ctx, "MetaDomProductid", auditRef, null, subDom); - - // put metadata with null productId - DomainMeta meta = zmsTestInitializer.createDomainMetaObject("Test sub Domain", "NewOrg", - true, true, "12345", null); - zmsImpl.putDomainMeta(ctx, "MetaDomProductid.metaSubDom", auditRef, null, meta); - - // put metadata with a productId - meta = zmsTestInitializer.createDomainMetaObject("Test sub Domain", "NewOrg", - true, true, "12345", ZMSTestInitializer.getRandomProductId()); - zmsImpl.putDomainMeta(ctx, "MetaDomProductid.metaSubDom", auditRef, null, meta); - - // set the expiry days to 30 - - meta.setMemberExpiryDays(30); - meta.setServiceExpiryDays(25); - meta.setGroupExpiryDays(35); - zmsImpl.putDomainMeta(ctx, "MetaDomProductid.metaSubDom", auditRef, null, meta); - Domain domain = zmsImpl.getDomain(ctx, "MetaDomProductid.metaSubDom"); - assertEquals(domain.getMemberExpiryDays(), Integer.valueOf(30)); - assertEquals(domain.getServiceExpiryDays(), Integer.valueOf(25)); - assertEquals(domain.getGroupExpiryDays(), Integer.valueOf(35)); - - // if value is null we're not going to change it - - meta.setMemberExpiryDays(null); - meta.setServiceExpiryDays(null); - meta.setGroupExpiryDays(null); - meta.setDescription("test1"); - zmsImpl.putDomainMeta(ctx, "MetaDomProductid.metaSubDom", auditRef, null, meta); - domain = zmsImpl.getDomain(ctx, "MetaDomProductid.metaSubDom"); - assertEquals(domain.getMemberExpiryDays(), Integer.valueOf(30)); - assertEquals(domain.getServiceExpiryDays(), Integer.valueOf(25)); - assertEquals(domain.getGroupExpiryDays(), Integer.valueOf(35)); - assertEquals(domain.getDescription(), "test1"); - - // setting is to 0 - - meta.setMemberExpiryDays(0); - meta.setServiceExpiryDays(0); - meta.setGroupExpiryDays(0); - meta.setDescription("test2"); - zmsImpl.putDomainMeta(ctx, "MetaDomProductid.metaSubDom", auditRef, null, meta); - domain = zmsImpl.getDomain(ctx, "MetaDomProductid.metaSubDom"); - assertNull(domain.getMemberExpiryDays()); - assertNull(domain.getServiceExpiryDays()); - assertNull(domain.getGroupExpiryDays()); - assertEquals(domain.getDescription(), "test2"); - - zmsImpl.deleteSubDomain(ctx, "MetaDomProductid", "metaSubDom", auditRef, null); - zmsImpl.deleteTopLevelDomain(ctx, "MetaDomProductid", auditRef, null); - } - @Test public void testGetRoleList() { @@ -29380,641 +28860,6 @@ public void testGetAthenzDomainWithEntities() { zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); } - @Test - public void testPutDomainMetaBusinessService() { - - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - final String auditRef = zmsTestInitializer.getAuditRef(); - - final String domainName = "athenz-domain-with-business-service"; - TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, - "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); - - Domain domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertNull(domain.getBusinessService()); - - // set the business service - - DomainMeta dm = new DomainMeta().setBusinessService("service1"); - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); - - domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertEquals(domain.getBusinessService(), "service1"); - - // update the business service - - dm.setBusinessService("service2"); - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); - - domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertEquals(domain.getBusinessService(), "service2"); - - // update different meta attribute - - dm = new DomainMeta().setDescription("new description"); - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); - - domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertEquals(domain.getBusinessService(), "service2"); - assertEquals(domain.getDescription(), "new description"); - - // remove the business service - - dm = new DomainMeta().setBusinessService("").setDescription("new description"); - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); - - domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertNull(domain.getBusinessService()); - assertEquals(domain.getDescription(), "new description"); - - zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); - } - - @Test - public void testPutDomainMetaEnvironment() { - - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - final String auditRef = zmsTestInitializer.getAuditRef(); - - final String domainName = "athenz-domain-with-environment"; - TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, - "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); - - Domain domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertNull(domain.getEnvironment()); - - // set the environment - - DomainMeta dm = new DomainMeta().setEnvironment("production"); - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); - - domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertEquals(domain.getEnvironment(), "production"); - - // update the environment - - dm.setEnvironment("staging"); - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); - - domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertEquals(domain.getEnvironment(), "staging"); - - // set an invalid value and verify failure - - dm = new DomainMeta().setEnvironment("unknown"); - try { - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid environment for domain")); - } - - // remove the environment - - dm = new DomainMeta().setEnvironment(""); - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); - - domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertNull(domain.getEnvironment()); - - zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); - } - - @Test - public void testPostDomainInvalidDomainMetaStoreValues() { - - final String domainName = "athenz-domain-with-invalid-details"; - - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - final String auditRef = zmsTestInitializer.getAuditRef(); - - DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; - zmsImpl.domainMetaStore = new TestDomainMetaStore(); - - TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, - "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); - - try { - dom1.setBusinessService("invalid-business-service"); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid business service name")); - } - - try { - dom1.setBusinessService("valid-business-service"); - dom1.setAccount("invalid-aws-account"); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid aws account")); - } - - 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) { - assertTrue(ex.getMessage().contains("invalid azure subscription")); - } - - try { - dom1.setAzureSubscription("valid-azure-subscription"); - dom1.setGcpProject("invalid-gcp-project"); - dom1.setGcpProjectNumber("1200"); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid gcp project")); - } - - zmsImpl.productIdSupport = true; - try { - dom1.setGcpProject("valid-gcp-project"); - dom1.setGcpProjectNumber("1200"); - dom1.setYpmId(100); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid product id")); - } - - try { - dom1.setYpmId(101); - dom1.setProductId("invalid-product-id"); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid product id")); - } - - // specify azure subscription but no tenant - - try { - dom1.setProductId("valid-product-id"); - 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(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid gcp project")); - } - - dom1.setGcpProjectNumber("1200"); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); - - Domain domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - 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); - zmsImpl.domainMetaStore = savedMetaStore; - zmsImpl.productIdSupport = false; - } - - @Test - public void testPutDomainMetaInvalidDomainMetaStoreValues() { - - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - final String auditRef = zmsTestInitializer.getAuditRef(); - - final String domainName = "athenz-domain-meta-with-invalid-details"; - DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; - zmsImpl.domainMetaStore = new TestDomainMetaStore(); - - TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, - "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); - - DomainMeta meta = new DomainMeta().setBusinessService("invalid-business-service"); - try { - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid business service name")); - } - - meta.setBusinessService("valid-business-service"); - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); - - // second time no-op since value not changed - - zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); - - Domain domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertEquals(domain.getBusinessService(), "valid-business-service"); - - zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); - zmsImpl.domainMetaStore = savedMetaStore; - } - - @Test - public void testPutDomainSystemMetaInvalidDomainMetaStoreValues() { - - final String domainName = "athenz-domain-system-meta-with-invalid-details"; - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - final String auditRef = zmsTestInitializer.getAuditRef(); - - DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; - zmsImpl.domainMetaStore = new TestDomainMetaStore(); - - TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, - "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); - - // first aws account - - DomainMeta meta = new DomainMeta().setAccount("invalid-aws-account"); - try { - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_ACCOUNT, auditRef, meta); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid aws account")); - } - - meta.setAccount("valid-aws-account"); - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_ACCOUNT, auditRef, meta); - - Domain domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertEquals(domain.getAccount(), "valid-aws-account"); - - // second time no-op since nothing has changed - - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_ACCOUNT, auditRef, meta); - - // 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 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")); - } - - // 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 - - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_AZURE_SUBSCRIPTION, auditRef, meta); - - // next gcp project - - try { - meta.setGcpProject("invalid-gcp-project"); - meta.setGcpProjectNumber("1200"); - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_GCP_PROJECT, auditRef, meta); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid gcp project")); - } - - // next gcp project without project number - - try { - meta.setGcpProject("valid-gcp-project"); - meta.setGcpProjectNumber(null); - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_GCP_PROJECT, auditRef, meta); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid gcp project")); - } - - meta.setGcpProject("valid-gcp-project"); - meta.setGcpProjectNumber("1200"); - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_GCP_PROJECT, auditRef, meta); - - domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertEquals(domain.getGcpProject(), "valid-gcp-project"); - assertEquals(domain.getGcpProjectNumber(), "1200"); - - // now keep the gcp project but update the project number - - meta.setGcpProject("valid-gcp-project"); - meta.setGcpProjectNumber("1201"); - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_GCP_PROJECT, auditRef, meta); - - domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertEquals(domain.getGcpProject(), "valid-gcp-project"); - assertEquals(domain.getGcpProjectNumber(), "1201"); - - // second time no-op since nothing has changed - - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_GCP_PROJECT, auditRef, meta); - - // next product id - - zmsImpl.productIdSupport = true; - try { - meta.setYpmId(100); - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_PRODUCT_ID, auditRef, meta); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid product id")); - } - - meta.setYpmId(101); - try { - meta.setProductId("invalid-product-id"); - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_PRODUCT_ID, auditRef, meta); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid product id")); - } - - meta.setProductId("valid-product-id"); - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_PRODUCT_ID, auditRef, meta); - - domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertEquals(domain.getYpmId().intValue(), 101); - - // final business service - - try { - meta.setBusinessService("invalid-business-service"); - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_BUSINESS_SERVICE, auditRef, meta); - fail(); - } catch (ResourceException ex) { - assertTrue(ex.getMessage().contains("invalid business service")); - } - - meta.setBusinessService("valid-business-service"); - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_BUSINESS_SERVICE, auditRef, meta); - - domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertEquals(domain.getBusinessService(), "valid-business-service"); - - // second time no-op since nothing has changed - - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_BUSINESS_SERVICE, auditRef, meta); - - zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); - zmsImpl.domainMetaStore = savedMetaStore; - zmsImpl.productIdSupport = false; - } - - @Test - public void testPutDomainMetaIDomainMetaStoreException() { - - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - final String auditRef = zmsTestInitializer.getAuditRef(); - - final String domainName = "athenz-domain-meta-with-exception"; - DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; - zmsImpl.domainMetaStore = new TestDomainMetaStore(); - - // value with exc- will throw an exception but we should - // not reject the request - - TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, - "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); - dom1.setBusinessService("exc-business-service"); - zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); - - Domain domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertEquals(domain.getBusinessService(), "exc-business-service"); - - // try with system attribute now as well - - DomainMeta meta = new DomainMeta().setAccount("exc-aws-account"); - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_ACCOUNT, auditRef, meta); - - domain = zmsImpl.getDomain(ctx, domainName); - assertNotNull(domain); - assertEquals(domain.getAccount(), "exc-aws-account"); - assertEquals(domain.getBusinessService(), "exc-business-service"); - - zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); - zmsImpl.domainMetaStore = savedMetaStore; - } - - @Test - public void testPutDomainSystemMetaInvalidDomain() { - - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - final String auditRef = zmsTestInitializer.getAuditRef(); - - final String domainName = "athenz-domain-system-meta-not-found"; - - DomainMeta meta = new DomainMeta().setAccount("aws-account"); - try { - zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_ACCOUNT, auditRef, meta); - fail(); - } catch (ResourceException ex) { - assertEquals(ex.getCode(), ResourceException.NOT_FOUND); - } - } - - @Test - public void testGetDomainMetaStoreValidValuesList() { - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - - DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; - DomainMetaStore mockDomainMetaStore = Mockito.mock(DomainMetaStore.class); - List awsAccountsList = Collections.singletonList("awsAcc"); - when(mockDomainMetaStore.getValidAWSAccounts(isNull())).thenReturn(awsAccountsList); - List businessServicesList = Collections.singletonList("bservice"); - when(mockDomainMetaStore.getValidBusinessServices(isNull())).thenReturn(businessServicesList); - List azureList = Collections.singletonList("azureSub"); - when(mockDomainMetaStore.getValidAzureSubscriptions(isNull())).thenReturn(azureList); - List gcpList = Collections.singletonList("gcpProject"); - when(mockDomainMetaStore.getValidGcpProjects(isNull())).thenReturn(gcpList); - List productIdList = Collections.singletonList("product"); - when(mockDomainMetaStore.getValidProductIds(isNull())).thenReturn(productIdList); - zmsImpl.domainMetaStore = mockDomainMetaStore; - assertEquals("bservice", zmsImpl.getDomainMetaStoreValidValuesList(ctx, "businessService", null).getValidValues().get(0)); - assertEquals("awsAcc", zmsImpl.getDomainMetaStoreValidValuesList(ctx, "awsAccount", null).getValidValues().get(0)); - assertEquals("azureSub", zmsImpl.getDomainMetaStoreValidValuesList(ctx, "azureSubscription", null).getValidValues().get(0)); - assertEquals("gcpProject", zmsImpl.getDomainMetaStoreValidValuesList(ctx, "gcpProject", null).getValidValues().get(0)); - assertEquals("product", zmsImpl.getDomainMetaStoreValidValuesList(ctx, "productId", null).getValidValues().get(0)); - assertEquals("product", zmsImpl.getDomainMetaStoreValidValuesList(ctx, "productNumber", null).getValidValues().get(0)); - zmsImpl.domainMetaStore = savedMetaStore; - } - - @Test - public void testGetDomainMetaStoreValidValuesListEmpty() { - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - - DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; - zmsImpl.domainMetaStore = new TestDomainMetaStore(); - DomainMetaStoreValidValuesList emptyValidValuesList = new DomainMetaStoreValidValuesList(); - emptyValidValuesList.setValidValues(new ArrayList<>()); - assertEquals(emptyValidValuesList, zmsImpl.getDomainMetaStoreValidValuesList(ctx, "businessService", null)); - assertEquals(emptyValidValuesList, zmsImpl.getDomainMetaStoreValidValuesList(ctx, "awsAccount", null)); - assertEquals(emptyValidValuesList, zmsImpl.getDomainMetaStoreValidValuesList(ctx, "azureSubscription", null)); - assertEquals(emptyValidValuesList, zmsImpl.getDomainMetaStoreValidValuesList(ctx, "gcpProject", null)); - assertEquals(emptyValidValuesList, zmsImpl.getDomainMetaStoreValidValuesList(ctx, "productId", null)); - zmsImpl.domainMetaStore = savedMetaStore; - } - - @Test - public void testGetDomainMetaStoreValidValuesListBadAttribute() { - - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - - DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; - zmsImpl.domainMetaStore = new TestDomainMetaStore(); - try { - zmsImpl.getDomainMetaStoreValidValuesList(ctx, "badAttribute", null); - fail(); - } catch (ResourceException ex) { - assertEquals(ex.getMessage(), "ResourceException (400): {code: 400, message: \"Invalid attribute: badAttribute\"}"); - } finally { - zmsImpl.domainMetaStore = savedMetaStore; - } - } - - @Test - public void testGetDomainMetaStoreValidValuesListMissingAttribute() { - - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - - DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; - zmsImpl.domainMetaStore = new TestDomainMetaStore(); - try { - zmsImpl.getDomainMetaStoreValidValuesList(ctx, null, null); - fail(); - } catch (ResourceException ex) { - assertEquals(ex.getMessage(), "ResourceException (400): {code: 400, message: \"attributeName is mandatory\"}"); - } finally { - zmsImpl.domainMetaStore = savedMetaStore; - } - } - - @Test - public void testGetDomainMetaStoreValidValuesUsernameLowered() { - - ZMSImpl zmsImpl = zmsTestInitializer.getZms(); - RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); - - DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; - DomainMetaStore mockDomainMetaStore = Mockito.mock(DomainMetaStore.class); - List businessServicesList = Collections.singletonList("bservice"); - when(mockDomainMetaStore.getValidBusinessServices(anyString())).thenReturn(businessServicesList); - - zmsImpl.domainMetaStore = mockDomainMetaStore; - ArgumentCaptor userCapture = ArgumentCaptor.forClass(String.class); - zmsImpl.getDomainMetaStoreValidValuesList(ctx, "businessService", "TestUser"); - verify(mockDomainMetaStore, times(1)).getValidBusinessServices(userCapture.capture()); - - assertEquals(userCapture.getValue(), "testuser"); - zmsImpl.domainMetaStore = savedMetaStore; - } - @Test public void testGetUserAuthorityAttributeMap() { ZMSImpl zmsImpl = zmsTestInitializer.getZms(); diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSMetaAttributeTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSMetaAttributeTest.java new file mode 100644 index 00000000000..ef530ed1e23 --- /dev/null +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSMetaAttributeTest.java @@ -0,0 +1,1426 @@ +/* + * 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.zms; + +import com.yahoo.athenz.common.server.metastore.DomainMetaStore; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.*; +import static org.testng.Assert.*; + +public class ZMSMetaAttributeTest { + + private final ZMSTestInitializer zmsTestInitializer = new ZMSTestInitializer(); + + @BeforeClass + public void startMemoryMySQL() { + zmsTestInitializer.startMemoryMySQL(); + } + + @AfterClass + public void stopMemoryMySQL() { + zmsTestInitializer.stopMemoryMySQL(); + } + + @BeforeMethod + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + zmsTestInitializer.setUp(); + } + + @Test + public void testPutDomainMetaBusinessService() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "athenz-domain-with-business-service"; + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + Domain domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertNull(domain.getBusinessService()); + + // set the business service + + DomainMeta dm = new DomainMeta().setBusinessService("service1"); + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getBusinessService(), "service1"); + + // update the business service + + dm.setBusinessService("service2"); + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getBusinessService(), "service2"); + + // update different meta attribute + + dm = new DomainMeta().setDescription("new description"); + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getBusinessService(), "service2"); + assertEquals(domain.getDescription(), "new description"); + + // remove the business service + + dm = new DomainMeta().setBusinessService("").setDescription("new description"); + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertNull(domain.getBusinessService()); + assertEquals(domain.getDescription(), "new description"); + + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + } + + @Test + public void testPutDomainMetaEnvironment() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "athenz-domain-with-environment"; + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + Domain domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertNull(domain.getEnvironment()); + + // set the environment + + DomainMeta dm = new DomainMeta().setEnvironment("production"); + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getEnvironment(), "production"); + + // update the environment + + dm.setEnvironment("staging"); + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getEnvironment(), "staging"); + + // set an invalid value and verify failure + + dm = new DomainMeta().setEnvironment("unknown"); + try { + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid environment for domain")); + } + + // remove the environment + + dm = new DomainMeta().setEnvironment(""); + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, dm); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertNull(domain.getEnvironment()); + + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + } + + @Test + public void testPostDomainInvalidDomainMetaStoreValues() { + + final String domainName = "athenz-domain-with-invalid-details"; + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; + zmsImpl.domainMetaStore = new TestDomainMetaStore(); + + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); + + try { + dom1.setBusinessService("invalid-business-service"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid business service name")); + } + + try { + dom1.setBusinessService("valid-business-service"); + dom1.setAccount("invalid-aws-account"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid aws account")); + } + + 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) { + assertTrue(ex.getMessage().contains("invalid azure subscription")); + } + + try { + dom1.setAzureSubscription("valid-azure-subscription"); + dom1.setGcpProject("invalid-gcp-project"); + dom1.setGcpProjectNumber("1200"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid gcp project")); + } + + zmsImpl.productIdSupport = true; + try { + dom1.setGcpProject("valid-gcp-project"); + dom1.setGcpProjectNumber("1200"); + dom1.setYpmId(100); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid product id")); + } + + try { + dom1.setYpmId(101); + dom1.setProductId("invalid-product-id"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid product id")); + } + + // specify azure subscription but no tenant + + try { + dom1.setProductId("valid-product-id"); + 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(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid gcp project")); + } + + dom1.setGcpProjectNumber("1200"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + Domain domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + 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); + zmsImpl.domainMetaStore = savedMetaStore; + zmsImpl.productIdSupport = false; + } + + @Test + public void testPutDomainMetaInvalidDomainMetaStoreValues() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "athenz-domain-meta-with-invalid-details"; + DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; + zmsImpl.domainMetaStore = new TestDomainMetaStore(); + + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + DomainMeta meta = new DomainMeta().setBusinessService("invalid-business-service"); + try { + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid business service name")); + } + + meta.setBusinessService("valid-business-service"); + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); + + // second time no-op since value not changed + + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); + + Domain domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getBusinessService(), "valid-business-service"); + + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + zmsImpl.domainMetaStore = savedMetaStore; + } + + @Test + public void testPutDomainSystemMetaInvalidDomainMetaStoreValues() { + + final String domainName = "athenz-domain-system-meta-with-invalid-details"; + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; + zmsImpl.domainMetaStore = new TestDomainMetaStore(); + + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + // first aws account + + DomainMeta meta = new DomainMeta().setAccount("invalid-aws-account"); + try { + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_ACCOUNT, auditRef, meta); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid aws account")); + } + + meta.setAccount("valid-aws-account"); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_ACCOUNT, auditRef, meta); + + Domain domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getAccount(), "valid-aws-account"); + + // second time no-op since nothing has changed + + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_ACCOUNT, auditRef, meta); + + // 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 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")); + } + + // 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 + + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_AZURE_SUBSCRIPTION, auditRef, meta); + + // next gcp project + + try { + meta.setGcpProject("invalid-gcp-project"); + meta.setGcpProjectNumber("1200"); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_GCP_PROJECT, auditRef, meta); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid gcp project")); + } + + // next gcp project without project number + + try { + meta.setGcpProject("valid-gcp-project"); + meta.setGcpProjectNumber(null); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_GCP_PROJECT, auditRef, meta); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid gcp project")); + } + + meta.setGcpProject("valid-gcp-project"); + meta.setGcpProjectNumber("1200"); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_GCP_PROJECT, auditRef, meta); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getGcpProject(), "valid-gcp-project"); + assertEquals(domain.getGcpProjectNumber(), "1200"); + + // now keep the gcp project but update the project number + + meta.setGcpProject("valid-gcp-project"); + meta.setGcpProjectNumber("1201"); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_GCP_PROJECT, auditRef, meta); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getGcpProject(), "valid-gcp-project"); + assertEquals(domain.getGcpProjectNumber(), "1201"); + + // second time no-op since nothing has changed + + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_GCP_PROJECT, auditRef, meta); + + // next product id + + zmsImpl.productIdSupport = true; + try { + meta.setYpmId(100); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_PRODUCT_ID, auditRef, meta); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid product id")); + } + + meta.setYpmId(101); + try { + meta.setProductId("invalid-product-id"); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_PRODUCT_ID, auditRef, meta); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid product id")); + } + + meta.setProductId("valid-product-id"); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_PRODUCT_ID, auditRef, meta); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getYpmId().intValue(), 101); + + // final business service + + try { + meta.setBusinessService("invalid-business-service"); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_BUSINESS_SERVICE, auditRef, meta); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("invalid business service")); + } + + meta.setBusinessService("valid-business-service"); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_BUSINESS_SERVICE, auditRef, meta); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getBusinessService(), "valid-business-service"); + + // second time no-op since nothing has changed + + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_BUSINESS_SERVICE, auditRef, meta); + + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + zmsImpl.domainMetaStore = savedMetaStore; + zmsImpl.productIdSupport = false; + } + + @Test + public void testPutDomainMetaIDomainMetaStoreException() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "athenz-domain-meta-with-exception"; + DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; + zmsImpl.domainMetaStore = new TestDomainMetaStore(); + + // value with exc- will throw an exception but we should + // not reject the request + + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); + dom1.setBusinessService("exc-business-service"); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + Domain domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getBusinessService(), "exc-business-service"); + + // try with system attribute now as well + + DomainMeta meta = new DomainMeta().setAccount("exc-aws-account"); + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_ACCOUNT, auditRef, meta); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getAccount(), "exc-aws-account"); + assertEquals(domain.getBusinessService(), "exc-business-service"); + + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + zmsImpl.domainMetaStore = savedMetaStore; + } + + @Test + public void testPutDomainSystemMetaInvalidDomain() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "athenz-domain-system-meta-not-found"; + + DomainMeta meta = new DomainMeta().setAccount("aws-account"); + try { + zmsImpl.putDomainSystemMeta(ctx, domainName, ZMSConsts.SYSTEM_META_ACCOUNT, auditRef, meta); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.NOT_FOUND); + } + } + + @Test + public void testGetDomainMetaStoreValidValuesList() { + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + + DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; + DomainMetaStore mockDomainMetaStore = Mockito.mock(DomainMetaStore.class); + List awsAccountsList = Collections.singletonList("awsAcc"); + when(mockDomainMetaStore.getValidAWSAccounts(isNull())).thenReturn(awsAccountsList); + List businessServicesList = Collections.singletonList("bservice"); + when(mockDomainMetaStore.getValidBusinessServices(isNull())).thenReturn(businessServicesList); + List azureList = Collections.singletonList("azureSub"); + when(mockDomainMetaStore.getValidAzureSubscriptions(isNull())).thenReturn(azureList); + List gcpList = Collections.singletonList("gcpProject"); + when(mockDomainMetaStore.getValidGcpProjects(isNull())).thenReturn(gcpList); + List productIdList = Collections.singletonList("product"); + when(mockDomainMetaStore.getValidProductIds(isNull())).thenReturn(productIdList); + zmsImpl.domainMetaStore = mockDomainMetaStore; + assertEquals("bservice", zmsImpl.getDomainMetaStoreValidValuesList(ctx, "businessService", null).getValidValues().get(0)); + assertEquals("awsAcc", zmsImpl.getDomainMetaStoreValidValuesList(ctx, "awsAccount", null).getValidValues().get(0)); + assertEquals("azureSub", zmsImpl.getDomainMetaStoreValidValuesList(ctx, "azureSubscription", null).getValidValues().get(0)); + assertEquals("gcpProject", zmsImpl.getDomainMetaStoreValidValuesList(ctx, "gcpProject", null).getValidValues().get(0)); + assertEquals("product", zmsImpl.getDomainMetaStoreValidValuesList(ctx, "productId", null).getValidValues().get(0)); + assertEquals("product", zmsImpl.getDomainMetaStoreValidValuesList(ctx, "productNumber", null).getValidValues().get(0)); + zmsImpl.domainMetaStore = savedMetaStore; + } + + @Test + public void testGetDomainMetaStoreValidValuesListEmpty() { + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + + DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; + zmsImpl.domainMetaStore = new TestDomainMetaStore(); + DomainMetaStoreValidValuesList emptyValidValuesList = new DomainMetaStoreValidValuesList(); + emptyValidValuesList.setValidValues(new ArrayList<>()); + assertEquals(emptyValidValuesList, zmsImpl.getDomainMetaStoreValidValuesList(ctx, "businessService", null)); + assertEquals(emptyValidValuesList, zmsImpl.getDomainMetaStoreValidValuesList(ctx, "awsAccount", null)); + assertEquals(emptyValidValuesList, zmsImpl.getDomainMetaStoreValidValuesList(ctx, "azureSubscription", null)); + assertEquals(emptyValidValuesList, zmsImpl.getDomainMetaStoreValidValuesList(ctx, "gcpProject", null)); + assertEquals(emptyValidValuesList, zmsImpl.getDomainMetaStoreValidValuesList(ctx, "productId", null)); + zmsImpl.domainMetaStore = savedMetaStore; + } + + @Test + public void testGetDomainMetaStoreValidValuesListBadAttribute() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + + DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; + zmsImpl.domainMetaStore = new TestDomainMetaStore(); + try { + zmsImpl.getDomainMetaStoreValidValuesList(ctx, "badAttribute", null); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getMessage(), "ResourceException (400): {code: 400, message: \"Invalid attribute: badAttribute\"}"); + } finally { + zmsImpl.domainMetaStore = savedMetaStore; + } + } + + @Test + public void testGetDomainMetaStoreValidValuesListMissingAttribute() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + + DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; + zmsImpl.domainMetaStore = new TestDomainMetaStore(); + try { + zmsImpl.getDomainMetaStoreValidValuesList(ctx, null, null); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getMessage(), "ResourceException (400): {code: 400, message: \"attributeName is mandatory\"}"); + } finally { + zmsImpl.domainMetaStore = savedMetaStore; + } + } + + @Test + public void testGetDomainMetaStoreValidValuesUsernameLowered() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + + DomainMetaStore savedMetaStore = zmsImpl.domainMetaStore; + DomainMetaStore mockDomainMetaStore = Mockito.mock(DomainMetaStore.class); + List businessServicesList = Collections.singletonList("bservice"); + when(mockDomainMetaStore.getValidBusinessServices(anyString())).thenReturn(businessServicesList); + + zmsImpl.domainMetaStore = mockDomainMetaStore; + ArgumentCaptor userCapture = ArgumentCaptor.forClass(String.class); + zmsImpl.getDomainMetaStoreValidValuesList(ctx, "businessService", "TestUser"); + verify(mockDomainMetaStore, times(1)).getValidBusinessServices(userCapture.capture()); + + assertEquals(userCapture.getValue(), "testuser"); + zmsImpl.domainMetaStore = savedMetaStore; + } + + @Test + public void testPutDomainMetaThrowException() { + + ZMSImplTest.TestAuditLogger alogger = new ZMSImplTest.TestAuditLogger(); + ZMSImpl zmsImpl = zmsTestInitializer.getZmsImpl(alogger); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + String domName = "wrongDomainName"; + DomainMeta meta = new DomainMeta(); + meta.setYpmId(ZMSTestInitializer.getRandomProductId()); + try { + zmsImpl.putDomainMeta(ctx, domName, auditRef, null, meta); + fail("notfounderror not thrown."); + } catch (ResourceException e) { + assertEquals(404, e.getCode()); + } + } + + @Test + public void testPutDomainMeta() { + + final String domainName = "domain-meta-test"; + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + Domain resDom1 = zmsImpl.getDomain(ctx, domainName); + assertNotNull(resDom1); + assertEquals(resDom1.getDescription(), "Test Domain1"); + assertEquals(resDom1.getOrg(), "testorg"); + assertTrue(resDom1.getEnabled()); + assertFalse(resDom1.getAuditEnabled()); + assertNull(resDom1.getServiceCertExpiryMins()); + assertNull(resDom1.getRoleCertExpiryMins()); + assertNull(resDom1.getMemberExpiryDays()); + assertNull(resDom1.getServiceExpiryDays()); + assertNull(resDom1.getGroupExpiryDays()); + assertNull(resDom1.getTokenExpiryMins()); + assertNull(resDom1.getMemberPurgeExpiryDays()); + assertNull(resDom1.getProductId()); + + DomainMeta meta = zmsTestInitializer.createDomainMetaObject("Test2 Domain", "NewOrg", + true, true, "12345", 1001); + meta.setCertDnsDomain("YAHOO.cloud"); + meta.setServiceCertExpiryMins(100); + meta.setRoleCertExpiryMins(200); + meta.setMemberPurgeExpiryDays(90); + meta.setSignAlgorithm("ec"); + meta.setProductId("abcd-1234"); + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); + zmsImpl.putDomainSystemMeta(ctx, domainName, "auditenabled", auditRef, meta); + zmsImpl.putDomainSystemMeta(ctx, domainName, "account", auditRef, meta); + zmsImpl.putDomainSystemMeta(ctx, domainName, "certdnsdomain", auditRef, meta); + + zmsTestInitializer.setupPrincipalSystemMetaDelete(zmsImpl, ctx.principal().getFullName(), + domainName, "domain", "productid", "org", "certdnsdomain"); + zmsImpl.putDomainSystemMeta(ctx, domainName, "org", auditRef, meta); + zmsImpl.putDomainSystemMeta(ctx, domainName, "productid", auditRef, meta); + + Domain resDom3 = zmsImpl.getDomain(ctx, domainName); + assertNotNull(resDom3); + assertEquals(resDom3.getDescription(), "Test2 Domain"); + assertEquals(resDom3.getOrg(), "neworg"); + assertTrue(resDom3.getEnabled()); + assertTrue(resDom3.getAuditEnabled()); + assertEquals(resDom3.getAccount(), "12345"); + assertEquals(Integer.valueOf(1001), resDom3.getYpmId()); + assertEquals(resDom3.getProductId(), "abcd-1234"); + assertEquals(resDom3.getCertDnsDomain(), "yahoo.cloud"); + assertEquals(resDom3.getServiceCertExpiryMins(), Integer.valueOf(100)); + assertEquals(resDom3.getMemberPurgeExpiryDays(), Integer.valueOf(90)); + assertEquals(resDom3.getRoleCertExpiryMins(), Integer.valueOf(200)); + assertNull(resDom3.getMemberExpiryDays()); + assertNull(resDom3.getServiceExpiryDays()); + assertNull(resDom3.getGroupExpiryDays()); + assertNull(resDom3.getTokenExpiryMins()); + assertEquals(resDom3.getSignAlgorithm(), "ec"); + + // put the metadata using same product id + + meta = zmsTestInitializer.createDomainMetaObject("just a new desc", "organs", + true, true, "12345", 1001); + meta.setMemberExpiryDays(300); + meta.setServiceExpiryDays(350); + meta.setGroupExpiryDays(375); + meta.setTokenExpiryMins(400); + meta.setProductId("abcd-1234"); + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); + + resDom3 = zmsImpl.getDomain(ctx, domainName); + assertNotNull(resDom3); + assertEquals(resDom3.getDescription(), "just a new desc"); + //org is system attr. so it won't be changed by putdomainmeta call + assertEquals(resDom3.getOrg(), "neworg"); + assertTrue(resDom3.getEnabled()); + assertTrue(resDom3.getAuditEnabled()); + assertEquals(resDom3.getAccount(), "12345"); + assertEquals(resDom3.getProductId(), "abcd-1234"); + assertEquals(Integer.valueOf(1001), resDom3.getYpmId()); + assertEquals(resDom3.getServiceCertExpiryMins(), Integer.valueOf(100)); + assertEquals(resDom3.getRoleCertExpiryMins(), Integer.valueOf(200)); + assertEquals(resDom3.getMemberExpiryDays(), Integer.valueOf(300)); + assertEquals(resDom3.getServiceExpiryDays(), Integer.valueOf(350)); + assertEquals(resDom3.getGroupExpiryDays(), Integer.valueOf(375)); + assertEquals(resDom3.getTokenExpiryMins(), Integer.valueOf(400)); + assertEquals(resDom3.getMemberPurgeExpiryDays(), Integer.valueOf(90)); + + zmsImpl.putDomainSystemMeta(ctx, domainName, "org", auditRef, meta); + resDom3 = zmsImpl.getDomain(ctx, domainName); + assertNotNull(resDom3); + assertEquals(resDom3.getOrg(), "organs"); + + // put the metadata using new product + meta = zmsTestInitializer.createDomainMetaObject("just a new desc", "organs", + true, true, "12345", 1001); + Integer newProductId = ZMSTestInitializer.getRandomProductId(); + meta.setYpmId(newProductId); + meta.setProductId("abcd-1234-5678"); + meta.setServiceCertExpiryMins(5); + meta.setRoleCertExpiryMins(0); + meta.setMemberExpiryDays(15); + meta.setServiceExpiryDays(17); + meta.setGroupExpiryDays(18); + meta.setTokenExpiryMins(20); + meta.setMemberPurgeExpiryDays(120); + meta.setSignAlgorithm("rsa"); + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); + zmsImpl.putDomainSystemMeta(ctx, domainName, "productid", auditRef, meta); + + resDom3 = zmsImpl.getDomain(ctx, domainName); + assertNotNull(resDom3); + assertEquals(resDom3.getDescription(), "just a new desc"); + assertEquals(resDom3.getOrg(), "organs"); + assertTrue(resDom3.getEnabled()); + assertTrue(resDom3.getAuditEnabled()); + assertEquals(resDom3.getAccount(), "12345"); + assertEquals(resDom3.getProductId(), "abcd-1234-5678"); + assertEquals(newProductId, resDom3.getYpmId()); + assertEquals(resDom3.getServiceCertExpiryMins(), Integer.valueOf(5)); + assertNull(resDom3.getRoleCertExpiryMins()); + assertEquals(resDom3.getMemberExpiryDays(), Integer.valueOf(15)); + assertEquals(resDom3.getServiceExpiryDays(), Integer.valueOf(17)); + assertEquals(resDom3.getGroupExpiryDays(), Integer.valueOf(18)); + assertEquals(resDom3.getTokenExpiryMins(), Integer.valueOf(20)); + assertEquals(resDom3.getMemberPurgeExpiryDays(), Integer.valueOf(120)); + assertEquals(resDom3.getSignAlgorithm(), "rsa"); + assertNull(resDom3.getFeatureFlags()); + + // put new feature flags for the domain + + meta.setFeatureFlags(3); + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); + zmsImpl.putDomainSystemMeta(ctx, domainName, "featureflags", auditRef, meta); + + resDom3 = zmsImpl.getDomain(ctx, domainName); + assertNotNull(resDom3); + assertEquals(resDom3.getDescription(), "just a new desc"); + assertEquals(resDom3.getOrg(), "organs"); + assertTrue(resDom3.getEnabled()); + assertTrue(resDom3.getAuditEnabled()); + assertEquals(resDom3.getAccount(), "12345"); + assertEquals(resDom3.getProductId(), "abcd-1234-5678"); + assertEquals(newProductId, resDom3.getYpmId()); + assertEquals(resDom3.getServiceCertExpiryMins(), Integer.valueOf(5)); + assertNull(resDom3.getRoleCertExpiryMins()); + assertEquals(resDom3.getMemberExpiryDays(), Integer.valueOf(15)); + assertEquals(resDom3.getServiceExpiryDays(), Integer.valueOf(17)); + assertEquals(resDom3.getGroupExpiryDays(), Integer.valueOf(18)); + assertEquals(resDom3.getTokenExpiryMins(), Integer.valueOf(20)); + assertEquals(resDom3.getMemberPurgeExpiryDays(), Integer.valueOf(120)); + assertEquals(resDom3.getSignAlgorithm(), "rsa"); + assertEquals(resDom3.getFeatureFlags().intValue(), 3); + + // update the feature flags value + + meta.setFeatureFlags(7); + zmsImpl.putDomainSystemMeta(ctx, domainName, "featureflags", auditRef, meta); + resDom3 = zmsImpl.getDomain(ctx, domainName); + assertEquals(resDom3.getFeatureFlags().intValue(), 7); + + zmsTestInitializer.cleanupPrincipalSystemMetaDelete(zmsImpl, "domain"); + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + } + + @Test + public void testPutDomainSystemMetaModifiedTimestamp() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "metadomainmodified"; + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + Domain resDom1 = zmsImpl.getDomain(ctx, domainName); + assertNotNull(resDom1); + long domMod1 = resDom1.getModified().millis(); + + ZMSTestUtils.sleep(1); + + DomainMeta meta = new DomainMeta(); + zmsImpl.putDomainSystemMeta(ctx, domainName, "modified", auditRef, meta); + + Domain resDom2 = zmsImpl.getDomain(ctx, domainName); + assertNotNull(resDom2); + long domMod2 = resDom2.getModified().millis(); + + assertTrue(domMod2 > domMod1); + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + } + + @Test + public void testPutDomainMetaInvalid() { + + // enable product id support + + System.setProperty(ZMSConsts.ZMS_PROP_PRODUCT_ID_SUPPORT, "true"); + ZMSImpl zmsImpl = zmsTestInitializer.zmsInit(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "MetaDomProductid"; + TopLevelDomain dom = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain", "testOrg", zmsTestInitializer.getAdminUser()); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom); + + Domain resDom = zmsImpl.getDomain(ctx, domainName); + assertNotNull(resDom); + assertEquals(resDom.getDescription(), "Test Domain"); + assertEquals(resDom.getOrg(), "testorg"); + assertTrue(resDom.getEnabled()); + assertFalse(resDom.getAuditEnabled()); + Integer productId = resDom.getYpmId(); + + zmsTestInitializer.setupPrincipalSystemMetaDelete(zmsImpl, ctx.principal().getFullName(), + domainName, "domain", "productid"); + DomainMeta meta = zmsTestInitializer.createDomainMetaObject("Test2 Domain", "NewOrg", + true, true, "12345", null); + try { + zmsImpl.putDomainSystemMeta(ctx, domainName, "productid", auditRef, meta); + fail("bad request exc not thrown"); + } catch (ResourceException exc) { + assertEquals(400, exc.getCode()); + assertTrue(exc.getMessage().contains("Unique Product Id must be specified for top level domain")); + } + + // put metadata using another domains productId + dom = zmsTestInitializer.createTopLevelDomainObject("MetaDomProductid2", + "Test Domain", "testOrg", zmsTestInitializer.getAdminUser()); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom); + + resDom = zmsImpl.getDomain(ctx, "MetaDomProductid2"); + Integer productId2 = resDom.getYpmId(); + assertNotEquals(productId, productId2); + + meta = zmsTestInitializer.createDomainMetaObject("Test3 Domain", "NewOrg", + true, true, "12345", productId2); + try { + zmsImpl.putDomainSystemMeta(ctx, domainName, "productid", auditRef, meta); + fail("bad request exc not thrown"); + } catch (ResourceException exc) { + assertEquals(400, exc.getCode()); + assertTrue(exc.getMessage().contains("is already assigned to domain")); + } + + // test negative values + + meta = new DomainMeta().setServiceExpiryDays(-10); + try { + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); + } + + meta = new DomainMeta().setGroupExpiryDays(-10); + try { + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); + } + + meta = new DomainMeta().setMemberExpiryDays(-10); + try { + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); + } + + meta = new DomainMeta().setRoleCertExpiryMins(-10); + try { + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); + } + + meta = new DomainMeta().setServiceCertExpiryMins(-10); + try { + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); + } + + meta = new DomainMeta().setTokenExpiryMins(-10); + try { + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); + } + + zmsTestInitializer.cleanupPrincipalSystemMetaDelete(zmsImpl, "domain"); + zmsImpl.deleteTopLevelDomain(ctx, "MetaDomProductid", auditRef, null); + zmsImpl.deleteTopLevelDomain(ctx, "MetaDomProductid2", auditRef, null); + System.clearProperty(ZMSConsts.ZMS_PROP_PRODUCT_ID_SUPPORT); + zmsImpl.objectStore.clearConnections(); + } + + @Test + public void testPutDomainMetaDefaults() { + + final String domainName = "meta-dom-values"; + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, null, null, + zmsTestInitializer.getAdminUser()); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + Domain resDom1 = zmsImpl.getDomain(ctx, domainName); + assertNotNull(resDom1); + assertNull(resDom1.getDescription()); + assertNull(resDom1.getOrg()); + assertTrue(resDom1.getEnabled()); + assertFalse(resDom1.getAuditEnabled()); + + DomainMeta meta = zmsTestInitializer.createDomainMetaObject("Test2 Domain", "NewOrg", true, false, null, 0); + zmsImpl.putDomainMeta(ctx, domainName, auditRef, null, meta); + + zmsImpl.putDomainSystemMeta(ctx, domainName, "org", auditRef, meta); + + Domain resDom3 = zmsImpl.getDomain(ctx, domainName); + assertNotNull(resDom3); + assertEquals(resDom3.getDescription(), "Test2 Domain"); + assertEquals(resDom3.getOrg(), "neworg"); + assertTrue(resDom3.getEnabled()); + assertFalse(resDom3.getAuditEnabled()); + assertNull(resDom3.getAccount()); + assertNull(resDom3.getAzureSubscription()); + assertNull(resDom3.getGcpProject()); + assertNull(resDom3.getBusinessService()); + assertEquals(Integer.valueOf(0), resDom3.getYpmId()); + + meta.setAccount("aws"); + zmsImpl.putDomainSystemMeta(ctx, domainName, "account", auditRef, meta); + resDom3 = zmsImpl.getDomain(ctx, domainName); + assertNotNull(resDom3); + assertEquals(resDom3.getOrg(), "neworg"); + assertEquals(resDom3.getAccount(), "aws"); + assertNull(resDom3.getAzureSubscription()); + assertNull(resDom3.getGcpProject()); + 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()); + + meta.setGcpProject("gcp"); + meta.setGcpProjectNumber("1239"); + zmsImpl.putDomainSystemMeta(ctx, domainName, "gcpproject", 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"); + assertEquals(resDom3.getGcpProject(), "gcp"); + assertEquals(resDom3.getGcpProjectNumber(), "1239"); + assertNull(resDom3.getBusinessService()); + + meta.setBusinessService("123:business service"); + zmsImpl.putDomainSystemMeta(ctx, domainName, "businessservice", 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"); + assertEquals(resDom3.getGcpProject(), "gcp"); + assertEquals(resDom3.getGcpProjectNumber(), "1239"); + assertEquals(resDom3.getBusinessService(), "123:business service"); + + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + } + + @Test + public void testPutDomainMetaMissingAuditRef() { + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + String domain = "testSetDomainMetaMissingAuditRef"; + TopLevelDomain dom = zmsTestInitializer.createTopLevelDomainObject( + domain, "Test1 Domain", "testOrg", zmsTestInitializer.getAdminUser()); + dom.setAuditEnabled(true); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom); + + Domain resDom = zmsImpl.getDomain(ctx, domain); + assertNotNull(resDom); + assertEquals(resDom.getDescription(), "Test1 Domain"); + assertEquals(resDom.getOrg(), "testorg"); + assertTrue(resDom.getAuditEnabled()); + + DomainMeta meta = zmsTestInitializer.createDomainMetaObject("Test2 Domain", "NewOrg", false, true, null, 0); + try { + zmsImpl.putDomainMeta(ctx, domain, null, null, meta); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 400); + assertTrue(ex.getMessage().contains("Audit reference required")); + } finally { + zmsImpl.deleteTopLevelDomain(ctx, domain, auditRef, null); + } + } + + @Test + public void testPutDomainMetaSubDomain() { + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + try { + TopLevelDomain dom = zmsTestInitializer.createTopLevelDomainObject("MetaDomProductid", + "Test Domain", "testOrg", zmsTestInitializer.getAdminUser(), ctx.principal().getFullName()); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom); + } catch (ResourceException rexc) { + assertEquals(400, rexc.getCode()); + } + + SubDomain subDom = zmsTestInitializer.createSubDomainObject("metaSubDom", "MetaDomProductid", + "sub Domain", "testOrg", zmsTestInitializer.getAdminUser()); + zmsImpl.postSubDomain(ctx, "MetaDomProductid", auditRef, null, subDom); + + // put metadata with null productId + DomainMeta meta = zmsTestInitializer.createDomainMetaObject("Test sub Domain", "NewOrg", + true, true, "12345", null); + zmsImpl.putDomainMeta(ctx, "MetaDomProductid.metaSubDom", auditRef, null, meta); + + // put metadata with a productId + meta = zmsTestInitializer.createDomainMetaObject("Test sub Domain", "NewOrg", + true, true, "12345", ZMSTestInitializer.getRandomProductId()); + zmsImpl.putDomainMeta(ctx, "MetaDomProductid.metaSubDom", auditRef, null, meta); + + // set the expiry days to 30 + + meta.setMemberExpiryDays(30); + meta.setServiceExpiryDays(25); + meta.setGroupExpiryDays(35); + zmsImpl.putDomainMeta(ctx, "MetaDomProductid.metaSubDom", auditRef, null, meta); + Domain domain = zmsImpl.getDomain(ctx, "MetaDomProductid.metaSubDom"); + assertEquals(domain.getMemberExpiryDays(), Integer.valueOf(30)); + assertEquals(domain.getServiceExpiryDays(), Integer.valueOf(25)); + assertEquals(domain.getGroupExpiryDays(), Integer.valueOf(35)); + + // if value is null we're not going to change it + + meta.setMemberExpiryDays(null); + meta.setServiceExpiryDays(null); + meta.setGroupExpiryDays(null); + meta.setDescription("test1"); + zmsImpl.putDomainMeta(ctx, "MetaDomProductid.metaSubDom", auditRef, null, meta); + domain = zmsImpl.getDomain(ctx, "MetaDomProductid.metaSubDom"); + assertEquals(domain.getMemberExpiryDays(), Integer.valueOf(30)); + assertEquals(domain.getServiceExpiryDays(), Integer.valueOf(25)); + assertEquals(domain.getGroupExpiryDays(), Integer.valueOf(35)); + assertEquals(domain.getDescription(), "test1"); + + // setting is to 0 + + meta.setMemberExpiryDays(0); + meta.setServiceExpiryDays(0); + meta.setGroupExpiryDays(0); + meta.setDescription("test2"); + zmsImpl.putDomainMeta(ctx, "MetaDomProductid.metaSubDom", auditRef, null, meta); + domain = zmsImpl.getDomain(ctx, "MetaDomProductid.metaSubDom"); + assertNull(domain.getMemberExpiryDays()); + assertNull(domain.getServiceExpiryDays()); + assertNull(domain.getGroupExpiryDays()); + assertEquals(domain.getDescription(), "test2"); + + zmsImpl.deleteSubDomain(ctx, "MetaDomProductid", "metaSubDom", auditRef, null); + zmsImpl.deleteTopLevelDomain(ctx, "MetaDomProductid", auditRef, null); + } + + @Test + public void testPutDomainSystemMetaX509CertSignerKeyId() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "athenz-domain-with-x509-cert-signer-key-id"; + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + Domain domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertNull(domain.getX509CertSignerKeyId()); + + // set the x509 cert signer key id + + DomainMeta dm = new DomainMeta().setX509CertSignerKeyId("x509-keyid"); + zmsImpl.putDomainSystemMeta(ctx, domainName, "x509certsignerkeyid", auditRef, dm); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getX509CertSignerKeyId(), "x509-keyid"); + + // update the x509 cert signer key id + // first we're going to be rejected with invalid authorization + + dm.setX509CertSignerKeyId("x509-keyid-2"); + try { + zmsImpl.putDomainSystemMeta(ctx, domainName, "x509certsignerkeyid", auditRef, dm); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("unauthorized to reset system meta attribute: x509certsignerkeyid")); + } + + // let's create the role and policy to allow this operation + + Role role1 = zmsTestInitializer.createRoleObject("sys.auth", "meta-cert-signer-keyid", null, "user.user1", + zmsTestInitializer.getAdminUser()); + zmsImpl.putRole(ctx, "sys.auth", "meta-cert-signer-keyid", auditRef, false, null, role1); + + Policy policy1 = zmsTestInitializer.createPolicyObject("sys.auth", "meta-cert-signer-keyid", + "meta-cert-signer-keyid", "delete", "sys.auth:meta.domain.x509certsignerkeyid.*", + AssertionEffect.ALLOW); + zmsImpl.putPolicy(ctx, "sys.auth", "meta-cert-signer-keyid", auditRef, false, null, policy1); + + // now our operation should succeed + + zmsImpl.putDomainSystemMeta(ctx, domainName, "x509certsignerkeyid", auditRef, dm); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getX509CertSignerKeyId(), "x509-keyid-2"); + + // set an invalid value and verify failure + + dm = new DomainMeta().setX509CertSignerKeyId("invalid key id"); + try { + zmsImpl.putDomainSystemMeta(ctx, domainName, "x509certsignerkeyid", auditRef, dm); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("Invalid CompoundName error")); + } + + // remove the x509 cert signer key id + + dm = new DomainMeta().setX509CertSignerKeyId(""); + zmsImpl.putDomainSystemMeta(ctx, domainName, "x509certsignerkeyid", auditRef, dm); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertNull(domain.getX509CertSignerKeyId()); + + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + } + + @Test + public void testPutDomainSystemMetaSshCertSignerKeyId() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "athenz-domain-with-ssh-cert-signer-key-id"; + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + Domain domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertNull(domain.getSshCertSignerKeyId()); + + // set the ssh cert signer key id + + DomainMeta dm = new DomainMeta().setSshCertSignerKeyId("ssh-keyid"); + zmsImpl.putDomainSystemMeta(ctx, domainName, "sshcertsignerkeyid", auditRef, dm); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getSshCertSignerKeyId(), "ssh-keyid"); + + // update the ssh cert signer key id + // first we're going to be rejected with invalid authorization + + dm.setSshCertSignerKeyId("ssh-keyid-2"); + try { + zmsImpl.putDomainSystemMeta(ctx, domainName, "sshcertsignerkeyid", auditRef, dm); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("unauthorized to reset system meta attribute: sshcertsignerkeyid")); + } + + // let's create the role and policy to allow this operation + + Role role1 = zmsTestInitializer.createRoleObject("sys.auth", "meta-cert-signer-keyid", null, "user.user1", + zmsTestInitializer.getAdminUser()); + zmsImpl.putRole(ctx, "sys.auth", "meta-cert-signer-keyid", auditRef, false, null, role1); + + Policy policy1 = zmsTestInitializer.createPolicyObject("sys.auth", "meta-cert-signer-keyid", + "meta-cert-signer-keyid", "delete", "sys.auth:meta.domain.sshcertsignerkeyid.*", + AssertionEffect.ALLOW); + zmsImpl.putPolicy(ctx, "sys.auth", "meta-cert-signer-keyid", auditRef, false, null, policy1); + + // now our operation should succeed + + zmsImpl.putDomainSystemMeta(ctx, domainName, "sshcertsignerkeyid", auditRef, dm); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertEquals(domain.getSshCertSignerKeyId(), "ssh-keyid-2"); + + // set an invalid value and verify failure + + dm = new DomainMeta().setSshCertSignerKeyId("invalid key id"); + try { + zmsImpl.putDomainSystemMeta(ctx, domainName, "sshcertsignerkeyid", auditRef, dm); + fail(); + } catch (ResourceException ex) { + assertTrue(ex.getMessage().contains("Invalid CompoundName error")); + } + + // remove the ssh cert signer key id + + dm = new DomainMeta().setSshCertSignerKeyId(""); + zmsImpl.putDomainSystemMeta(ctx, domainName, "sshcertsignerkeyid", auditRef, dm); + + domain = zmsImpl.getDomain(ctx, domainName); + assertNotNull(domain); + assertNull(domain.getX509CertSignerKeyId()); + + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + } + + @Test + public void testSubDomainSignerKeyIdInherit() { + + ZMSImpl zmsImpl = zmsTestInitializer.getZms(); + RsrcCtxWrapper ctx = zmsTestInitializer.getMockDomRsrcCtx(); + final String auditRef = zmsTestInitializer.getAuditRef(); + + final String domainName = "athenz-domain-inherit-signer-key"; + TopLevelDomain dom1 = zmsTestInitializer.createTopLevelDomainObject(domainName, + "Test Domain1", "testOrg", zmsTestInitializer.getAdminUser()); + zmsImpl.postTopLevelDomain(ctx, auditRef, null, dom1); + + // create subdomain and verify no signer key ids + + SubDomain subDom1 = zmsTestInitializer.createSubDomainObject("sub1", domainName, + "sub Domain", "testOrg", zmsTestInitializer.getAdminUser(), "user.user1"); + zmsImpl.postSubDomain(ctx, domainName, auditRef, null, subDom1); + + Domain domain = zmsImpl.getDomain(ctx, domainName + ".sub1"); + assertNotNull(domain); + assertNull(domain.getX509CertSignerKeyId()); + assertNull(domain.getSshCertSignerKeyId()); + + // now set the x509 and ssh cert signer key ids + + DomainMeta dm = new DomainMeta().setSshCertSignerKeyId("ssh-keyid") + .setX509CertSignerKeyId("x509-keyid"); + zmsImpl.putDomainSystemMeta(ctx, domainName, "sshcertsignerkeyid", auditRef, dm); + zmsImpl.putDomainSystemMeta(ctx, domainName, "x509certsignerkeyid", auditRef, dm); + + // create a new subdomain and verify the key ids are inherited + + SubDomain subDom2 = zmsTestInitializer.createSubDomainObject("sub2", domainName, + "sub Domain", "testOrg", zmsTestInitializer.getAdminUser(), "user.user1"); + zmsImpl.postSubDomain(ctx, domainName, auditRef, null, subDom2); + + domain = zmsImpl.getDomain(ctx, domainName + ".sub2"); + assertNotNull(domain); + assertEquals(domain.getSshCertSignerKeyId(), "ssh-keyid"); + assertEquals(domain.getX509CertSignerKeyId(), "x509-keyid"); + + // create another subdomain for the subdomain and verify the key ids are inherited + + SubDomain subDom3 = zmsTestInitializer.createSubDomainObject("sub3", domainName + ".sub2", + "sub Domain", "testOrg", zmsTestInitializer.getAdminUser(), "user.user1"); + zmsImpl.postSubDomain(ctx, domainName + ".sub2", auditRef, null, subDom3); + + domain = zmsImpl.getDomain(ctx, domainName + ".sub2.sub3"); + assertNotNull(domain); + assertEquals(domain.getSshCertSignerKeyId(), "ssh-keyid"); + assertEquals(domain.getX509CertSignerKeyId(), "x509-keyid"); + + zmsImpl.deleteSubDomain(ctx, domainName + ".sub2", "sub3", auditRef, null); + zmsImpl.deleteSubDomain(ctx, domainName, "sub2", auditRef, null); + zmsImpl.deleteSubDomain(ctx, domainName, "sub1", auditRef, null); + zmsImpl.deleteTopLevelDomain(ctx, domainName, auditRef, null); + } +} 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 d00c23d52d2..ad49aaecd41 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 @@ -88,6 +88,8 @@ public void testGetDomain() throws Exception { Mockito.doReturn("abcd-1234").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_PRODUCT_ID); Mockito.doReturn("production").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_ENVIRONMENT); Mockito.doReturn(3).when(mockResultSet).getInt(ZMSConsts.DB_COLUMN_FEATURE_FLAGS); + Mockito.doReturn("x509").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_X509_CERT_SIGNER_KEYID); + Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_SSH_CERT_SIGNER_KEYID); JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); Domain domain = jdbcConn.getDomain("my-domain"); @@ -105,6 +107,8 @@ public void testGetDomain() throws Exception { assertEquals(domain.getTags(), Collections.singletonMap("tag-key", new TagValueList().setList(Collections.singletonList("tag-val")))); assertEquals(domain.getFeatureFlags(), 3); assertEquals(domain.getEnvironment(), "production"); + assertEquals(domain.getX509CertSignerKeyId(), "x509"); + assertNull(domain.getSshCertSignerKeyId()); jdbcConn.close(); } @@ -136,6 +140,8 @@ public void testGetDomainWithAuditEnabled() throws Exception { Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_PRODUCT_ID); Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_ENVIRONMENT); Mockito.doReturn(0).when(mockResultSet).getInt(ZMSConsts.DB_COLUMN_FEATURE_FLAGS); + Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_X509_CERT_SIGNER_KEYID); + Mockito.doReturn("").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_SSH_CERT_SIGNER_KEYID); JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); Domain domain = jdbcConn.getDomain("my-domain"); @@ -423,6 +429,8 @@ public void testGetDomainAllFields() throws Exception { Mockito.doReturn("tag-val").when(mockResultSet).getString(2); Mockito.doReturn("abcd-1234").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_PRODUCT_ID); Mockito.doReturn("production").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_ENVIRONMENT); + Mockito.doReturn("x509").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_X509_CERT_SIGNER_KEYID); + Mockito.doReturn("ssh").when(mockResultSet).getString(ZMSConsts.DB_COLUMN_SSH_CERT_SIGNER_KEYID); Mockito.doReturn(1).when(mockResultSet).getInt(ZMSConsts.DB_COLUMN_FEATURE_FLAGS); JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); @@ -439,7 +447,8 @@ public void testGetDomainAllFields() throws Exception { assertEquals(domain.getTags(), Collections.singletonMap("tag-key", new TagValueList().setList(Collections.singletonList("tag-val")))); assertEquals(domain.getFeatureFlags(), 1); assertEquals(domain.getEnvironment(), "production"); - + assertEquals(domain.getX509CertSignerKeyId(), "x509"); + assertEquals(domain.getSshCertSignerKeyId(), "ssh"); jdbcConn.close(); } @@ -662,7 +671,9 @@ public void testUpdateDomain() throws Exception { .setGcpProjectNumber("1235") .setProductId("abcd-1234") .setFeatureFlags(3) - .setEnvironment("production"); + .setEnvironment("production") + .setSshCertSignerKeyId("ssh") + .setX509CertSignerKeyId("x509"); Mockito.doReturn(1).when(mockPrepStmt).executeUpdate(); boolean requestSuccess = jdbcConn.updateDomain(domain); @@ -695,7 +706,9 @@ public void testUpdateDomain() throws Exception { Mockito.verify(mockPrepStmt, times(1)).setString(25, "production"); 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"); + Mockito.verify(mockPrepStmt, times(1)).setString(28, "x509"); + Mockito.verify(mockPrepStmt, times(1)).setString(29, "ssh"); + Mockito.verify(mockPrepStmt, times(1)).setString(30, "my-domain"); jdbcConn.close(); } @@ -739,7 +752,9 @@ public void testUpdateDomainNullFields() throws Exception { Mockito.verify(mockPrepStmt, times(1)).setString(25, ""); Mockito.verify(mockPrepStmt, times(1)).setString(26, ""); Mockito.verify(mockPrepStmt, times(1)).setString(27, ""); - Mockito.verify(mockPrepStmt, times(1)).setString(28, "my-domain"); + Mockito.verify(mockPrepStmt, times(1)).setString(28, ""); + Mockito.verify(mockPrepStmt, times(1)).setString(29, ""); + Mockito.verify(mockPrepStmt, times(1)).setString(30, "my-domain"); jdbcConn.close(); } @@ -6469,6 +6484,8 @@ public void testListModifiedDomains() throws Exception { Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_BUSINESS_SERVICE)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_PRODUCT_ID)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_ENVIRONMENT)).thenReturn(""); + Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_X509_CERT_SIGNER_KEYID)).thenReturn(""); + Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_SSH_CERT_SIGNER_KEYID)).thenReturn(""); DomainMetaList list = jdbcConn.listModifiedDomains(1454358900); @@ -6629,6 +6646,8 @@ public void testGetAthenzDomain() throws Exception { Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_PRODUCT_ID)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_ENVIRONMENT)).thenReturn(""); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_PRINCIPAL_DOMAIN_FILTER)).thenReturn(""); + Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_X509_CERT_SIGNER_KEYID)).thenReturn(""); + Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_SSH_CERT_SIGNER_KEYID)).thenReturn(""); AthenzDomain athenzDomain = jdbcConn.getAthenzDomain("my-domain"); assertNotNull(athenzDomain);