Skip to content

Commit abf172e

Browse files
committed
fix: build and address review feedback
1 parent e73f44c commit abf172e

File tree

22 files changed

+541
-544
lines changed

22 files changed

+541
-544
lines changed

backend/src/@types/fastify.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,13 @@ import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/
9393
import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service";
9494
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
9595
import { TIdentityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service";
96-
import { TIdentitySpiffeAuthServiceFactory } from "@app/services/identity-spiffe-auth/identity-spiffe-auth-service";
9796
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
9897
import { TIdentityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service";
9998
import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
10099
import { TIdentityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service";
101100
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
102101
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
102+
import { TIdentitySpiffeAuthServiceFactory } from "@app/services/identity-spiffe-auth/identity-spiffe-auth-service";
103103
import { TIdentityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-types";
104104
import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
105105
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";

backend/src/db/migrations/20260305201413_add-spiffe-machine-auth.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export async function up(knex: Knex): Promise<void> {
1515
t.string("configurationType").notNullable();
1616
t.binary("encryptedCaBundleJwks").nullable();
1717
t.string("bundleEndpointUrl").nullable();
18-
t.string("bundleEndpointProfile").nullable();
1918
t.binary("encryptedBundleEndpointCaCert").nullable();
2019
t.binary("encryptedCachedBundleJwks").nullable();
2120
t.datetime("cachedBundleLastRefreshedAt").nullable();

backend/src/db/schemas/identity-spiffe-auths.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,10 @@ export const IdentitySpiffeAuthsSchema = z.object({
1818
configurationType: z.string(),
1919
encryptedCaBundleJwks: zodBuffer.nullable().optional(),
2020
bundleEndpointUrl: z.string().nullable().optional(),
21-
bundleEndpointProfile: z.string().nullable().optional(),
2221
encryptedBundleEndpointCaCert: zodBuffer.nullable().optional(),
2322
encryptedCachedBundleJwks: zodBuffer.nullable().optional(),
2423
cachedBundleLastRefreshedAt: z.date().nullable().optional(),
25-
bundleRefreshHintSeconds: z.coerce.number().default(300),
24+
bundleRefreshHintSeconds: z.number().default(300),
2625
accessTokenTTL: z.coerce.number().default(7200),
2726
accessTokenMaxTTL: z.coerce.number().default(7200),
2827
accessTokenNumUsesLimit: z.coerce.number().default(0),

backend/src/db/schemas/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ export * from "./identity-azure-auths";
6464
export * from "./identity-gcp-auths";
6565
export * from "./identity-group-membership";
6666
export * from "./identity-jwt-auths";
67-
export * from "./identity-spiffe-auths";
6867
export * from "./identity-kubernetes-auths";
6968
export * from "./identity-metadata";
7069
export * from "./identity-oci-auths";
@@ -73,6 +72,7 @@ export * from "./identity-org-memberships";
7372
export * from "./identity-project-additional-privilege";
7473
export * from "./identity-project-membership-role";
7574
export * from "./identity-project-memberships";
75+
export * from "./identity-spiffe-auths";
7676
export * from "./identity-tls-cert-auths";
7777
export * from "./identity-token-auths";
7878
export * from "./identity-ua-client-secrets";

backend/src/lib/api-docs/constants.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -758,17 +758,16 @@ export const SPIFFE_AUTH = {
758758
allowedSpiffeIds:
759759
"Comma-separated list of allowed SPIFFE ID patterns. Supports picomatch glob patterns (e.g. spiffe://prod.example.com/**).",
760760
allowedAudiences: "Comma-separated list of allowed audiences for JWT-SVID validation.",
761-
configurationType:
762-
"The configuration type for trust bundle management. Must be one of: 'static' (admin uploads JWKS), 'remote' (auto-refresh from SPIRE bundle endpoint).",
763-
caBundleJwks:
764-
"The JWKS JSON containing public keys for JWT-SVID verification. Required if configurationType is 'static'.",
765-
bundleEndpointUrl:
766-
"The SPIRE bundle endpoint URL for automatic trust bundle retrieval. Required if configurationType is 'remote'.",
767-
bundleEndpointProfile:
768-
"The bundle endpoint authentication profile. Must be one of: 'https_web' (standard HTTPS), 'https_spiffe' (mTLS with SPIFFE auth).",
769-
bundleEndpointCaCert:
770-
"The PEM-encoded CA certificate for verifying the bundle endpoint TLS connection. Required when bundleEndpointProfile is 'https_spiffe'.",
771-
bundleRefreshHintSeconds: "The interval in seconds between bundle refresh attempts. Defaults to 300.",
761+
trustBundleDistribution: {
762+
profile:
763+
"The trust bundle distribution profile. Must be one of: 'static' (admin uploads JWKS), 'https_web_bundle' (auto-refresh from HTTPS endpoint).",
764+
bundle: "The JWKS JSON containing public keys for JWT-SVID verification. Required when profile is 'static'.",
765+
endpointUrl:
766+
"The SPIRE bundle endpoint URL for automatic trust bundle retrieval. Required when profile is 'https_web_bundle'.",
767+
caCert:
768+
"Optional PEM-encoded root CA certificate for verifying the bundle endpoint TLS connection. Defaults to system root CAs when not provided.",
769+
refreshHintSeconds: "The interval in seconds between bundle refresh attempts. Defaults to 3600."
770+
},
772771
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
773772
accessTokenTTL: "The lifetime for an access token in seconds.",
774773
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
@@ -779,12 +778,13 @@ export const SPIFFE_AUTH = {
779778
trustDomain: "The new SPIFFE trust domain.",
780779
allowedSpiffeIds: "The new comma-separated list of allowed SPIFFE ID patterns.",
781780
allowedAudiences: "The new comma-separated list of allowed audiences.",
782-
configurationType: "The new configuration type for trust bundle management.",
783-
caBundleJwks: "The new JWKS JSON containing public keys.",
784-
bundleEndpointUrl: "The new SPIRE bundle endpoint URL.",
785-
bundleEndpointProfile: "The new bundle endpoint authentication profile.",
786-
bundleEndpointCaCert: "The new PEM-encoded CA certificate for the bundle endpoint.",
787-
bundleRefreshHintSeconds: "The new interval in seconds between bundle refresh attempts.",
781+
trustBundleDistribution: {
782+
profile: "The new trust bundle distribution profile.",
783+
bundle: "The new JWKS JSON containing public keys.",
784+
endpointUrl: "The new SPIRE bundle endpoint URL.",
785+
caCert: "The new PEM-encoded CA certificate for the bundle endpoint.",
786+
refreshHintSeconds: "The new interval in seconds between bundle refresh attempts."
787+
},
788788
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
789789
accessTokenTTL: "The new lifetime for an access token in seconds.",
790790
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",

backend/src/server/routes/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,6 @@ import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/ident
285285
import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
286286
import { identityJwtAuthDALFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-dal";
287287
import { identityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service";
288-
import { identitySpiffeAuthDALFactory } from "@app/services/identity-spiffe-auth/identity-spiffe-auth-dal";
289-
import { identitySpiffeAuthServiceFactory } from "@app/services/identity-spiffe-auth/identity-spiffe-auth-service";
290288
import { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal";
291289
import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
292290
import { identityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal";
@@ -297,6 +295,8 @@ import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/ide
297295
import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
298296
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
299297
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
298+
import { identitySpiffeAuthDALFactory } from "@app/services/identity-spiffe-auth/identity-spiffe-auth-dal";
299+
import { identitySpiffeAuthServiceFactory } from "@app/services/identity-spiffe-auth/identity-spiffe-auth-service";
300300
import { identityTlsCertAuthDALFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-dal";
301301
import { identityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-service";
302302
import { identityTokenAuthDALFactory } from "@app/services/identity-token-auth/identity-token-auth-dal";
@@ -1972,7 +1972,6 @@ export const registerRoutes = async (
19721972
membershipIdentityDAL
19731973
});
19741974

1975-
19761975
const identityLdapAuthService = identityLdapAuthServiceFactory({
19771976
identityLdapAuthDAL,
19781977
orgDAL,

backend/src/server/routes/v1/identity-spiffe-auth-router.ts

Lines changed: 65 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,79 @@ import { slugSchema } from "@app/server/lib/schemas";
88
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
99
import { AuthMode } from "@app/services/auth/auth-type";
1010
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
11-
import {
12-
SpiffeBundleEndpointProfile,
13-
SpiffeConfigurationType
14-
} from "@app/services/identity-spiffe-auth/identity-spiffe-auth-types";
11+
import { SpiffeTrustBundleProfile } from "@app/services/identity-spiffe-auth/identity-spiffe-auth-types";
1512
import {
1613
validateSpiffeAllowedAudiencesField,
1714
validateSpiffeAllowedIdsField,
1815
validateTrustDomain
1916
} from "@app/services/identity-spiffe-auth/identity-spiffe-auth-validators";
2017
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
2118

22-
const IdentitySpiffeAuthResponseSchema = IdentitySpiffeAuthsSchema.omit({
23-
encryptedCaBundleJwks: true,
24-
encryptedBundleEndpointCaCert: true,
25-
encryptedCachedBundleJwks: true
19+
const StaticTrustBundleSchema = z.object({
20+
profile: z.literal(SpiffeTrustBundleProfile.STATIC).describe(SPIFFE_AUTH.ATTACH.trustBundleDistribution.profile),
21+
bundle: z.string().min(1).describe(SPIFFE_AUTH.ATTACH.trustBundleDistribution.bundle)
22+
});
23+
24+
const HttpsWebBundleSchema = z.object({
25+
profile: z
26+
.literal(SpiffeTrustBundleProfile.HTTPS_WEB_BUNDLE)
27+
.describe(SPIFFE_AUTH.ATTACH.trustBundleDistribution.profile),
28+
endpointUrl: z
29+
.string()
30+
.trim()
31+
.url()
32+
.refine((url) => url.startsWith("https://"), "Bundle endpoint URL must use HTTPS")
33+
.describe(SPIFFE_AUTH.ATTACH.trustBundleDistribution.endpointUrl),
34+
caCert: z.string().optional().describe(SPIFFE_AUTH.ATTACH.trustBundleDistribution.caCert),
35+
refreshHintSeconds: z
36+
.number()
37+
.int()
38+
.min(0)
39+
.default(3600)
40+
.describe(SPIFFE_AUTH.ATTACH.trustBundleDistribution.refreshHintSeconds)
41+
});
42+
43+
const TrustBundleDistributionSchema = z.discriminatedUnion("profile", [StaticTrustBundleSchema, HttpsWebBundleSchema]);
44+
45+
const StaticTrustBundleResponseSchema = z.object({
46+
profile: z.literal(SpiffeTrustBundleProfile.STATIC),
47+
bundle: z.string()
48+
});
49+
50+
const HttpsWebBundleResponseSchema = z.object({
51+
profile: z.literal(SpiffeTrustBundleProfile.HTTPS_WEB_BUNDLE),
52+
endpointUrl: z.string(),
53+
caCert: z.string(),
54+
refreshHintSeconds: z.number(),
55+
cachedBundleLastRefreshedAt: z.date().nullable().optional()
56+
});
57+
58+
const TrustBundleDistributionResponseSchema = z.discriminatedUnion("profile", [
59+
StaticTrustBundleResponseSchema,
60+
HttpsWebBundleResponseSchema
61+
]);
62+
63+
const IdentitySpiffeAuthResponseSchema = IdentitySpiffeAuthsSchema.pick({
64+
id: true,
65+
identityId: true,
66+
trustDomain: true,
67+
allowedSpiffeIds: true,
68+
allowedAudiences: true,
69+
accessTokenTTL: true,
70+
accessTokenMaxTTL: true,
71+
accessTokenNumUsesLimit: true,
72+
accessTokenTrustedIps: true,
73+
createdAt: true,
74+
updatedAt: true
2675
}).extend({
27-
caBundleJwks: z.string(),
28-
bundleEndpointCaCert: z.string()
76+
trustBundleDistribution: TrustBundleDistributionResponseSchema
2977
});
3078

3179
const CommonCreateFields = z.object({
3280
trustDomain: validateTrustDomain.describe(SPIFFE_AUTH.ATTACH.trustDomain),
3381
allowedSpiffeIds: validateSpiffeAllowedIdsField.describe(SPIFFE_AUTH.ATTACH.allowedSpiffeIds),
3482
allowedAudiences: validateSpiffeAllowedAudiencesField.describe(SPIFFE_AUTH.ATTACH.allowedAudiences),
83+
trustBundleDistribution: TrustBundleDistributionSchema,
3584
accessTokenTrustedIps: z
3685
.object({
3786
ipAddress: z.string().trim()
@@ -51,68 +100,7 @@ const CommonCreateFields = z.object({
51100
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(SPIFFE_AUTH.ATTACH.accessTokenNumUsesLimit)
52101
});
53102

54-
const CommonUpdateFields = z
55-
.object({
56-
trustDomain: validateTrustDomain.describe(SPIFFE_AUTH.UPDATE.trustDomain),
57-
allowedSpiffeIds: validateSpiffeAllowedIdsField.describe(SPIFFE_AUTH.UPDATE.allowedSpiffeIds),
58-
allowedAudiences: validateSpiffeAllowedAudiencesField.describe(SPIFFE_AUTH.UPDATE.allowedAudiences),
59-
accessTokenTrustedIps: z
60-
.object({
61-
ipAddress: z.string().trim()
62-
})
63-
.array()
64-
.min(1)
65-
.describe(SPIFFE_AUTH.UPDATE.accessTokenTrustedIps),
66-
accessTokenTTL: z
67-
.number()
68-
.int()
69-
.min(0)
70-
.max(315360000)
71-
.describe(SPIFFE_AUTH.UPDATE.accessTokenTTL),
72-
accessTokenMaxTTL: z
73-
.number()
74-
.int()
75-
.min(0)
76-
.max(315360000)
77-
.describe(SPIFFE_AUTH.UPDATE.accessTokenMaxTTL),
78-
accessTokenNumUsesLimit: z.number().int().min(0).describe(SPIFFE_AUTH.UPDATE.accessTokenNumUsesLimit)
79-
})
80-
.partial();
81-
82-
const StaticConfigurationSchema = z.object({
83-
configurationType: z
84-
.literal(SpiffeConfigurationType.STATIC)
85-
.describe(SPIFFE_AUTH.ATTACH.configurationType),
86-
caBundleJwks: z.string().min(1).describe(SPIFFE_AUTH.ATTACH.caBundleJwks),
87-
bundleEndpointUrl: z.string().optional().default(""),
88-
bundleEndpointProfile: z.nativeEnum(SpiffeBundleEndpointProfile).optional(),
89-
bundleEndpointCaCert: z.string().optional().default(""),
90-
bundleRefreshHintSeconds: z.number().int().min(0).optional().default(3600)
91-
});
92-
93-
const RemoteConfigurationSchema = z.object({
94-
configurationType: z
95-
.literal(SpiffeConfigurationType.REMOTE)
96-
.describe(SPIFFE_AUTH.ATTACH.configurationType),
97-
caBundleJwks: z.string().optional().default(""),
98-
bundleEndpointUrl: z
99-
.string()
100-
.trim()
101-
.url()
102-
.refine((url) => url.startsWith("https://"), "Bundle endpoint URL must use HTTPS")
103-
.describe(SPIFFE_AUTH.ATTACH.bundleEndpointUrl),
104-
bundleEndpointProfile: z
105-
.nativeEnum(SpiffeBundleEndpointProfile)
106-
.default(SpiffeBundleEndpointProfile.HTTPS_WEB)
107-
.describe(SPIFFE_AUTH.ATTACH.bundleEndpointProfile),
108-
bundleEndpointCaCert: z.string().optional().default("").describe(SPIFFE_AUTH.ATTACH.bundleEndpointCaCert),
109-
bundleRefreshHintSeconds: z
110-
.number()
111-
.int()
112-
.min(0)
113-
.default(3600)
114-
.describe(SPIFFE_AUTH.ATTACH.bundleRefreshHintSeconds)
115-
});
103+
const CommonUpdateFields = CommonCreateFields.partial();
116104

117105
export const registerIdentitySpiffeAuthRouter = async (server: FastifyZodProvider) => {
118106
server.route({
@@ -185,10 +173,7 @@ export const registerIdentitySpiffeAuthRouter = async (server: FastifyZodProvide
185173
params: z.object({
186174
identityId: z.string().trim().describe(SPIFFE_AUTH.ATTACH.identityId)
187175
}),
188-
body: z.discriminatedUnion("configurationType", [
189-
StaticConfigurationSchema.merge(CommonCreateFields),
190-
RemoteConfigurationSchema.merge(CommonCreateFields)
191-
]),
176+
body: CommonCreateFields,
192177
response: {
193178
200: z.object({
194179
identitySpiffeAuth: IdentitySpiffeAuthResponseSchema
@@ -216,7 +201,7 @@ export const registerIdentitySpiffeAuthRouter = async (server: FastifyZodProvide
216201
trustDomain: identitySpiffeAuth.trustDomain,
217202
allowedSpiffeIds: identitySpiffeAuth.allowedSpiffeIds,
218203
allowedAudiences: identitySpiffeAuth.allowedAudiences,
219-
configurationType: identitySpiffeAuth.configurationType,
204+
configurationType: identitySpiffeAuth.trustBundleDistribution.profile,
220205
accessTokenTTL: identitySpiffeAuth.accessTokenTTL,
221206
accessTokenMaxTTL: identitySpiffeAuth.accessTokenMaxTTL,
222207
accessTokenTrustedIps: identitySpiffeAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
@@ -251,10 +236,7 @@ export const registerIdentitySpiffeAuthRouter = async (server: FastifyZodProvide
251236
params: z.object({
252237
identityId: z.string().trim().describe(SPIFFE_AUTH.UPDATE.identityId)
253238
}),
254-
body: z.discriminatedUnion("configurationType", [
255-
StaticConfigurationSchema.merge(CommonUpdateFields),
256-
RemoteConfigurationSchema.merge(CommonUpdateFields)
257-
]),
239+
body: CommonUpdateFields,
258240
response: {
259241
200: z.object({
260242
identitySpiffeAuth: IdentitySpiffeAuthResponseSchema
@@ -281,7 +263,7 @@ export const registerIdentitySpiffeAuthRouter = async (server: FastifyZodProvide
281263
trustDomain: identitySpiffeAuth.trustDomain,
282264
allowedSpiffeIds: identitySpiffeAuth.allowedSpiffeIds,
283265
allowedAudiences: identitySpiffeAuth.allowedAudiences,
284-
configurationType: identitySpiffeAuth.configurationType,
266+
configurationType: identitySpiffeAuth.trustBundleDistribution.profile,
285267
accessTokenTTL: identitySpiffeAuth.accessTokenTTL,
286268
accessTokenMaxTTL: identitySpiffeAuth.accessTokenMaxTTL,
287269
accessTokenTrustedIps: identitySpiffeAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
@@ -416,10 +398,7 @@ export const registerIdentitySpiffeAuthRouter = async (server: FastifyZodProvide
416398
}),
417399
response: {
418400
200: z.object({
419-
identitySpiffeAuth: IdentitySpiffeAuthResponseSchema.omit({
420-
caBundleJwks: true,
421-
bundleEndpointCaCert: true
422-
})
401+
identitySpiffeAuth: IdentitySpiffeAuthResponseSchema
423402
})
424403
}
425404
},

backend/src/server/routes/v1/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,14 @@ import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
3838
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
3939
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
4040
import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router";
41-
import { registerIdentitySpiffeAuthRouter } from "./identity-spiffe-auth-router";
4241
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
4342
import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
4443
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
4544
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
4645
import { registerIdentityOrgMembershipRouter } from "./identity-org-membership-router";
4746
import { registerIdentityProjectMembershipRouter } from "./identity-project-membership-router";
4847
import { registerIdentityRouter } from "./identity-router";
48+
import { registerIdentitySpiffeAuthRouter } from "./identity-spiffe-auth-router";
4949
import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router";
5050
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
5151
import { registerIdentityUaRouter } from "./identity-universal-auth-router";

0 commit comments

Comments
 (0)