From 8d8a1a0e33563de4409899e140970c16b835cd44 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Tue, 9 Jan 2024 10:37:04 +0000 Subject: [PATCH 001/126] chore(add keycloak to compose): adds keycloak to the compose with a preconfigured realm and client (#1123) * chore(add keycloak to compose): adds keycloak to the compose with a preconfigured realm and client * chore(add keycloak to compose): adds keycloak to the compose with a preconfigured realm and client * chore(update port): update port * chore(compose): increase retires for healthcheck --- docker-compose.yaml | 22 + local/keycloak/jimm-realm.json | 2261 ++++++++++++++++++++++++++++++++ 2 files changed, 2283 insertions(+) create mode 100644 local/keycloak/jimm-realm.json diff --git a/docker-compose.yaml b/docker-compose.yaml index 8ab90c4d0..e231af986 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -93,6 +93,8 @@ services: condition: service_healthy insert-hardcoded-auth-model: condition: service_completed_successfully + keycloak: + condition: service_healthy labels: traefik.enable: true traefik.http.routers.jimm.rule: Host(`jimm.localhost`) @@ -222,3 +224,23 @@ services: depends_on: openfga: condition: service_healthy + + keycloak: + image: docker.io/bitnami/keycloak:23 + environment: + KEYCLOAK_HTTP_PORT: 8082 + KEYCLOAK_ENABLE_HEALTH_ENDPOINTS: true + KEYCLOAK_CREATE_ADMIN_USER: true + KEYCLOAK_ADMIN_USER: jimm + KEYCLOAK_ADMIN_PASSWORD: jimm + KEYCLOAK_DATABASE_VENDOR: dev-mem + KEYCLOAK_EXTRA_ARGS: "-Dkeycloak.migration.action=import -Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=/bitnami/keycloak/data/import/realm.json -Dkeycloak.migration.replace-placeholders=true -Dkeycloak.profile.feature.upload_scripts=enabled" + volumes: + - ./local/keycloak/jimm-realm.json:/bitnami/keycloak/data/import/realm.json:ro + ports: + - "8082:8082" + healthcheck: + test: [ "CMD", "curl", "http://0.0.0.0:8082/health/started" ] + interval: 5s + timeout: 5s + retries: 30 diff --git a/local/keycloak/jimm-realm.json b/local/keycloak/jimm-realm.json new file mode 100644 index 000000000..edae7b871 --- /dev/null +++ b/local/keycloak/jimm-realm.json @@ -0,0 +1,2261 @@ +{ + "id": "b307f672-e789-4f4d-bb54-051f67686046", + "realm": "jimm", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "73355732-169f-4c0f-8fea-db0794ef5a55", + "name": "default-roles-jimm", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "view-profile", + "manage-account" + ] + } + }, + "clientRole": false, + "containerId": "b307f672-e789-4f4d-bb54-051f67686046", + "attributes": {} + }, + { + "id": "88184f3a-18cc-4d30-af30-a6196075a5aa", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "b307f672-e789-4f4d-bb54-051f67686046", + "attributes": {} + }, + { + "id": "6802acab-ef3f-4d8a-b255-f6eedfcbac25", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "b307f672-e789-4f4d-bb54-051f67686046", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "b776bcfd-140f-4f83-a030-bf12e4060aac", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "062b6466-4c52-4abc-9925-f0dc43c4592e", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "dab02dd5-a48c-4522-b14f-d5c75b421df0", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "6f79a2c0-1c81-47cf-8b2d-a81e04000fb2", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "28335019-75c2-478b-b102-c5b759939c01", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "33e4615a-48ed-4a05-b21f-741a9990410d", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "88df5e21-3fa8-43f6-b0d3-db5f46ff6c7b", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "caabf5fb-5077-403e-a603-a5d562cae25b", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "4e67550d-4985-45af-b6ea-a05ddd5f5024", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-clients", + "manage-users", + "query-clients", + "query-users", + "view-realm", + "manage-identity-providers", + "query-realms", + "view-users", + "manage-realm", + "view-events", + "manage-events", + "view-identity-providers", + "manage-authorization", + "query-groups", + "impersonation", + "view-authorization", + "view-clients", + "create-client" + ] + } + }, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "85b76caf-c2ea-44a2-b357-3e4d2952fdd5", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "bb495a95-3958-4d08-a009-09a9dc4c739f", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "1c39f2fe-860c-46e0-9501-699efea014ff", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "96bfb00e-b453-4b33-8a6e-29338d05cff1", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "ac4c1b2b-b8b6-4999-9e06-5fd2b18926a7", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "d22d36e3-75b1-4696-8a1e-00cace42ced3", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "12fc3766-0b79-4777-9bde-847697ea91a5", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "f1e8e47a-682b-4103-93ef-6a154fd4a3a8", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "6e7fa140-0b3d-428c-ae09-345b398a1a12", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "d1efd1a2-e68d-4429-a6d0-92a0b318f70b", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "9150bc28-f740-4e8f-9ce3-13ff80fbd324", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "a335192a-14e7-4f47-ae24-d9b45a89fb77", + "attributes": {} + } + ], + "account": [ + { + "id": "cfcd7076-3f3e-42fe-9cc2-441122ac5542", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + }, + { + "id": "8ef797db-56e4-43b8-8e44-2ea8fdb5dc51", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + }, + { + "id": "900fb81f-669e-4fef-8e5b-e6702665acba", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + }, + { + "id": "781ba8fe-34e9-41f3-83ce-f2d52c024500", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + }, + { + "id": "43e59ee5-098a-45d6-8ba8-a7af5d86db2a", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + }, + { + "id": "b32c7271-04a8-4291-9ab8-d3a4e6879146", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + }, + { + "id": "e6352054-fc03-4d1e-acf9-6861f6bd6bd8", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + }, + { + "id": "ed221e99-697a-4745-9ab7-dc877643f2f6", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + } + ], + "jimm": [] + } + }, + "groups": [], + "defaultRole": { + "id": "73355732-169f-4c0f-8fea-db0794ef5a55", + "name": "default-roles-jimm", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "b307f672-e789-4f4d-bb54-051f67686046" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "users": [ + { + "id": "42d4a03f-3a15-4be2-a464-7726984a211d", + "createdTimestamp": 1704705192741, + "username": "service-account-jimm", + "enabled": true, + "totp": false, + "emailVerified": false, + "serviceAccountClientId": "jimm", + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-jimm" + ], + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/jimm/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/jimm/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "08730e1b-5e69-45ee-9078-7b28416b24de", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/jimm/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/jimm/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "714b0a7d-e1d3-4f27-b995-bd0aaa3ae2e1", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "5c17b099-c66d-4e74-991e-0fd22cb3050e", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "a335192a-14e7-4f47-ae24-d9b45a89fb77", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "9b8ea7fd-1ea8-43e6-9e2e-71b8f7d878e3", + "clientId": "jimm", + "name": "jimm-testing", + "description": "A client to enable testing JIMM", + "rootUrl": "http://localhost", + "adminUrl": "http://localhost", + "baseUrl": "http://localhost", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": true, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "http://localhost/cb" + ], + "webOrigins": [ + "*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": true, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1704705192", + "backchannel.logout.session.required": "true", + "consent.screen.text": "JIMM consent screen", + "login_theme": "base", + "post.logout.redirect.uris": "localhost:8080/logout", + "oauth2.device.authorization.grant.enabled": "true", + "display.on.consent.screen": "true", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "28687e0a-ff13-4a16-b8a8-80bd6bbccea9", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "introspection.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "1f6015f3-2812-41a9-896d-155d7d685ca6", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "introspection.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "id": "5f8f6d15-2f4d-4ce0-908b-9c5fd482e1b5", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "client_id", + "introspection.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "client_id", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "735cae33-7b02-4539-98af-a63bd09c850b", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/jimm/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/jimm/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "f1fdfb97-747e-441f-934d-c1eec0d02a1a", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "98a43368-13e9-4c5f-8511-95403c22ee64", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "97752e1e-f7ee-4ba6-91c7-451cfd020425", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "89e366f8-77b7-4428-a946-9c8f427460fd", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String" + } + }, + { + "id": "aad15d42-ad65-441d-8abd-e8c81b376082", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String" + } + }, + { + "id": "8f2d9913-f9ab-4cc2-90b0-7486162eb554", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "3924c40c-3941-44cd-a695-eaa7117c8d34", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "5b20276e-a01d-47fb-95d5-77c8eec04157", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "92b1456c-e05b-4349-969d-9622c011447d", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "87b09405-7bc8-4a0f-b7f4-4cb758d47cad", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "94faeffb-5579-4347-9801-a9b095efcc16", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "2a77708e-f986-4cf8-bce4-ffb29aaab30b", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "e100936f-87a9-4c74-b38e-30aa82413a48", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "140dd259-788a-48a1-b890-32c6e6104cf2", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "08acc240-8601-4235-9e30-c3a2035352e7", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "4d4806cb-3c58-49ef-8065-e327af8dc748", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "c958e838-0bb7-4aeb-b387-91147ca7e627", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "9019e06d-3949-4420-8025-5f977eeb94f7", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "b62b12bb-cdf6-4a96-a3d9-99dd6979ab36", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "045ffc8b-7898-4276-9feb-adf36c3b36c2", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "5e9d597f-8064-4b74-9853-93105fa8b643", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + } + ] + }, + { + "id": "d4241c04-74cb-4463-bc4d-0c038f183086", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "b58ff5e3-bc9a-464a-8877-119e491c8213", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "8e66dc5f-59ff-4e9d-bebf-45f43a350d4e", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "54eae197-3ef1-4c49-837d-23c8434b2b97", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "0bbada0c-5f98-4f75-8820-cb4ac3383bb6", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "04b49931-0780-4f62-b098-6de969fc5ce2", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "30260d43-3298-418a-9f5c-28e050a7e429", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "9fd9c2e6-6b26-4026-bb1b-1003c2e1934d", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "6b1105ca-f3c5-447b-ba38-66393e8b4ce7", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "9ccb464e-c78d-4c8f-8a24-4b4754d22537", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "019a385b-2631-41fb-a444-2d636105c506", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "a1ecc841-161d-4de6-a2a2-2b4fe964ff50", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "8d80ce4b-3a01-48f9-974f-6a5e2074ccc6", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "userinfo.token.claim": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "6e520aff-0c54-4f06-af15-8e77874d5145", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "8153fa75-5dfa-4caa-a52c-f39d61ee4a28", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "776d1bfd-6192-48a4-a9c8-67b33752bf28", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "440f0736-dac9-4a4d-918d-0255a674fb46", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "9f8786d7-0dbd-4c67-81cf-97d8870ae747", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "337e0eb8-71d0-472f-a402-6e8656fb2158", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-sha256-pairwise-sub-mapper", + "oidc-full-name-mapper", + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "oidc-address-mapper", + "saml-role-list-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-usermodel-property-mapper" + ] + } + }, + { + "id": "9aed0a59-b7e6-41f8-a862-83401d1f6a26", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "d1af4a25-c22a-4e0a-877d-671a13598e44", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "f9077f05-c3a6-4dcc-8db1-79b6e7f69366", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "0a184f0f-92bb-42c6-a0d4-a4cdabc38d76", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-full-name-mapper", + "oidc-address-mapper", + "saml-user-attribute-mapper", + "oidc-usermodel-attribute-mapper", + "saml-role-list-mapper", + "saml-user-property-mapper" + ] + } + }, + { + "id": "cf4d9180-6c03-4c81-897b-2654e1a9157a", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "8d7f40d4-70d5-4d58-919d-a35fc1cb589d", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "0c361a8c-35ab-4ba7-91b3-98ba3d388db4", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + }, + { + "id": "250164bf-1952-4139-b247-910f71a852ba", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "d74a3cae-1559-41ca-82e4-635192d76d41", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "205539ad-c37e-44ce-adea-4d598441e9cd", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "9b835aa1-820a-4e71-b41b-29397ff5d805", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "0eb823f9-df61-49b1-bde8-4749eb36a677", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "bf6d7a73-2723-4314-83c6-a9f8f137cd49", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "9d86c8e0-c5ad-401a-874e-fe41a2ac0362", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "2b399312-003c-4b09-af85-016bdb78f018", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "8c62d4bb-dd39-44fa-834d-978e6c0efd86", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "523dbb97-675d-4a82-b8d0-8f55f953cc5d", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "7484294b-3b44-4603-9ff3-3cc1d9f898b1", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "fb087480-2aaf-421f-a3e3-bab2f40f43cb", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "25f1d47f-fe02-4b05-8100-56229d8930ac", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "1c9030e5-5be1-44dd-bc9b-326e5fc8addc", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "dc2a8b23-b362-4f25-bf40-44259d5d8c11", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "0d7b1bf4-440e-4c32-a2b2-51476ff732bd", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "b7bf2c5c-d6d7-4ba0-a996-d786f18ba198", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "9342460d-847e-44fe-b248-bd8bfe62af2e", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "3a29625a-d3f2-4b99-a2e3-e7f3d44f1a69", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "8577b54d-6720-4cf0-82b1-29b22761862a", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "2c171aa0-b957-469b-a98a-a2f69dfc48f6", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "e6a925a0-fa76-4ea8-accc-548a2e7f97f8", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "c980449f-5ef2-447a-b78f-00a0e39bcfd6", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "clientOfflineSessionMaxLifespan": "0", + "oauth2DevicePollingInterval": "5", + "clientSessionIdleTimeout": "0", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5", + "realmReusableOtpCode": "false" + }, + "keycloakVersion": "23.0.3", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } + } \ No newline at end of file From ec9c5b53438472eff4723b51a27fc0d590b0847f Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Tue, 9 Jan 2024 15:23:56 +0000 Subject: [PATCH 002/126] =?UTF-8?q?docs(keyclaok=20readme):=20add=20a=20RE?= =?UTF-8?q?ADME=20to=20describe=20why=20keycloak,=20and=20det=E2=80=A6=20(?= =?UTF-8?q?#1124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- local/keycloak/README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 local/keycloak/README.md diff --git a/local/keycloak/README.md b/local/keycloak/README.md new file mode 100644 index 000000000..5e7dfa973 --- /dev/null +++ b/local/keycloak/README.md @@ -0,0 +1,40 @@ +# Keycloak + +## Why Keycloak? +As of 9th Jan, 2024, the company's desired OAuth2.0 server does NOT support the OAuth2.0 device flow, and it is required for JIMM to migrate away from Macaroons. As such, for local development, Keycloak has been chosen as it supports the device flow out of the box, including ALL other OAuth2.0 standard and extension grants and flows. + +## What is Keycloak? +Keycloak is an open-source identity and access management tool that supports standard IAM protocols such as OAuth 2.0, OpenID Connect, and SAML. It enables the creation of OAuth 2.0 clients for secure authorisation and authentication. + +The key features in this local development environment we require are the user management and storage, and the OAuth2.0 server. + +## What is a "Realm"? +A Keycloak realm is a security and administrative domain where users, applications, roles, and groups are managed. Realms are isolated from each other and can only manage and authenticate the users that they contain. Keycloak allows for the creation, storage, and management of multiple realms within a single deployment, providing a way to isolate and secure different sets of applications and users. The realm acts as a container for all the objects that make up your security domain. In our local development environment, we have a preconfigured realm that is imported on startup. + +## What is a "Client"? +In Keycloak, a client represents an application that can request authentication or authorisation. Clients can be web applications, service accounts, or other types of applications that interact with Keycloak for security purposes. They are registered within a specific realm and can be configured to use different authentication methods, such as client ID and client secret, signed JWT, or other supported mechanisms. Additionally, clients can define roles specific to them and are associated with client scopes, which are useful for sharing common settings and requesting claims or roles based on scope parameters. + +In the context of OpenID Connect, scopes are used to request specific sets of user details, such as name and email, during authentication. Each scope returns a set of user attributes, known as claims. The "openid" scope is required and indicates that an application intends to use the OpenID Connect protocol to verify a user's identity. Other scopes, such as "profile" and "email," allow applications to request additional user details. Claims are the assertions made about a subject, and scopes are groups of claims used for access control. + +## How does Keycloak differ from Hydra and Kratos? +Keycloak is considered an all-in-one solution, where as Kratos is strictly an Identity provider and Hydra is an OAuth2.0 server AND provider. For all intents and purposes, they are the same, but some language is different. + +Here are some synonymous terms: +- Keycloak Users -> Kratos Identites +- Keycloak Clients (SAML, OIDC, JWT, etc.) -> Hydra OAuth2.0 Client +- Keycloak Role Mappings -> Hydra Trait Mapping +- Keycloak Realm -> Ory Project + +## Further reading + +Keycloak: +- [Getting Started](https://www.keycloak.org/guides#getting-started) +- [Server Administration](https://www.keycloak.org/docs/latest/server_admin/index.html) + +Kratos: +- [Kratos Summary](https://www.ory.sh/docs/ecosystem/projects#ory-kratos) + +Hydra: +- [Quickstart](https://www.ory.sh/docs/hydra/5min-tutorial) +- [Hydra Summary](https://www.ory.sh/docs/ecosystem/projects#ory-hydra) + From 5a88bbad2a9e80e32ed2d25550b823a55893bd77 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Wed, 10 Jan 2024 12:19:46 +0200 Subject: [PATCH 003/126] CSS-6700 add service account to auth model (#1125) * Update authorisation_model.json * Added service account to names package * Update authorisation_model.json * Added ServiceAccountType where requried * Updated auth model * Update service account kind * Copyright and godoc change * Godoc and test tweaks --- internal/openfga/names/names.go | 6 +- internal/openfga/openfga.go | 20 +++--- local/openfga/authorisation_model.fga | 38 +++++++++++ local/openfga/authorisation_model.json | 92 +++++++++++++++++++++++++- pkg/names/names.go | 5 ++ pkg/names/service_account.go | 66 ++++++++++++++++++ pkg/names/service_account_test.go | 57 ++++++++++++++++ 7 files changed, 273 insertions(+), 11 deletions(-) create mode 100644 local/openfga/authorisation_model.fga create mode 100644 pkg/names/service_account.go create mode 100644 pkg/names/service_account_test.go diff --git a/internal/openfga/names/names.go b/internal/openfga/names/names.go index 74997168f..0843e2d17 100644 --- a/internal/openfga/names/names.go +++ b/internal/openfga/names/names.go @@ -58,7 +58,8 @@ type ResourceTagger interface { names.ControllerTag | names.ModelTag | names.ApplicationOfferTag | - names.CloudTag + names.CloudTag | + jimmnames.ServiceAccountTag Id() string Kind() string @@ -106,7 +107,8 @@ func BlankKindTag(kind string) (*Tag, error) { switch kind { case names.UserTagKind, jimmnames.GroupTagKind, names.ControllerTagKind, names.ModelTagKind, - names.ApplicationOfferTagKind, names.CloudTagKind: + names.ApplicationOfferTagKind, names.CloudTagKind, + jimmnames.ServiceAccountTagKind: return &Tag{ Kind: cofga.Kind(kind), }, nil diff --git a/internal/openfga/openfga.go b/internal/openfga/openfga.go index 0498d296b..b79c7fa98 100644 --- a/internal/openfga/openfga.go +++ b/internal/openfga/openfga.go @@ -16,7 +16,7 @@ import ( var ( // resourceTypes contains a list of all resource kinds (i.e. tags) used throughout JIMM. - resourceTypes = [...]string{names.UserTagKind, names.ModelTagKind, names.ControllerTagKind, names.ApplicationOfferTagKind, jimmnames.GroupTagKind} + resourceTypes = [...]string{names.UserTagKind, names.ModelTagKind, names.ControllerTagKind, names.ApplicationOfferTagKind, jimmnames.GroupTagKind, jimmnames.ServiceAccountTagKind} ) // Tuple represents a relation between an object and a target. @@ -34,20 +34,24 @@ type Kind = cofga.Kind // Relation holds the type of tag relation. type Relation = cofga.Relation -// Object Kinds +// Object Kinds, these are OpenFGA object Kinds that reference +// Juju/JIMM objects. These are included here for ease of use +// and avoiding string constants. var ( // ModelType represents a model object. - ModelType Kind = "model" + ModelType Kind = names.ModelTagKind // ApplicationOfferType represents an application offer object. - ApplicationOfferType Kind = "applicationoffer" + ApplicationOfferType Kind = jimmnames.ApplicationOfferTagKind // CloudType represents a cloud object. - CloudType Kind = "cloud" + CloudType Kind = names.CloudTagKind // ControllerType represents a controller object. - ControllerType Kind = "controller" + ControllerType Kind = names.ControllerTagKind // GroupType represents a group object. - GroupType Kind = "group" + GroupType Kind = jimmnames.GroupTagKind // UserType represents a user object. - UserType Kind = "user" + UserType Kind = names.UserTagKind + // ServiceAccountType represents a service account. + ServiceAccountType Kind = jimmnames.ServiceAccountTagKind ) // OFGAClient contains convenient utility methods for interacting diff --git a/local/openfga/authorisation_model.fga b/local/openfga/authorisation_model.fga new file mode 100644 index 000000000..365e17880 --- /dev/null +++ b/local/openfga/authorisation_model.fga @@ -0,0 +1,38 @@ +model + schema 1.1 + +type applicationoffer + relations + define administrator: [serviceaccount, serviceaccount:*, user, user:*, group#member] or administrator from model + define consumer: [serviceaccount, serviceaccount:*, user, user:*, group#member] or administrator + define model: [model] + define reader: [serviceaccount, serviceaccount:*, user, user:*, group#member] or consumer + +type cloud + relations + define administrator: [serviceaccount, serviceaccount:*, user, user:*, group#member] or administrator from controller + define can_addmodel: [serviceaccount, serviceaccount:*, user, user:*, group#member] or administrator + define controller: [controller] + +type controller + relations + define administrator: [user, user:*, group#member] or administrator from controller + define audit_log_viewer: [user, user:*, group#member] or administrator + define controller: [controller] + +type group + relations + define member: [user, user:*, group#member] + +type model + relations + define administrator: [serviceaccount, serviceaccount:*, user, user:*, group#member] or administrator from controller + define controller: [controller] + define reader: [serviceaccount, serviceaccount:*, user, user:*, group#member] or writer + define writer: [serviceaccount, serviceaccount:*, user, user:*, group#member] or administrator + +type user + +type serviceaccount + relations + define administator: [serviceaccount, serviceaccount:*, user, user:*, group#member] diff --git a/local/openfga/authorisation_model.json b/local/openfga/authorisation_model.json index e072de5ff..4ea5dff0b 100644 --- a/local/openfga/authorisation_model.json +++ b/local/openfga/authorisation_model.json @@ -6,6 +6,13 @@ "relations": { "administrator": { "directly_related_user_types": [ + { + "type": "serviceaccount" + }, + { + "type": "serviceaccount", + "wildcard": {} + }, { "type": "user" }, @@ -21,6 +28,13 @@ }, "consumer": { "directly_related_user_types": [ + { + "type": "serviceaccount" + }, + { + "type": "serviceaccount", + "wildcard": {} + }, { "type": "user" }, @@ -43,6 +57,13 @@ }, "reader": { "directly_related_user_types": [ + { + "type": "serviceaccount" + }, + { + "type": "serviceaccount", + "wildcard": {} + }, { "type": "user" }, @@ -117,6 +138,13 @@ "relations": { "administrator": { "directly_related_user_types": [ + { + "type": "serviceaccount" + }, + { + "type": "serviceaccount", + "wildcard": {} + }, { "type": "user" }, @@ -132,6 +160,13 @@ }, "can_addmodel": { "directly_related_user_types": [ + { + "type": "serviceaccount" + }, + { + "type": "serviceaccount", + "wildcard": {} + }, { "type": "user" }, @@ -308,6 +343,13 @@ "relations": { "administrator": { "directly_related_user_types": [ + { + "type": "serviceaccount" + }, + { + "type": "serviceaccount", + "wildcard": {} + }, { "type": "user" }, @@ -330,6 +372,13 @@ }, "reader": { "directly_related_user_types": [ + { + "type": "serviceaccount" + }, + { + "type": "serviceaccount", + "wildcard": {} + }, { "type": "user" }, @@ -345,6 +394,13 @@ }, "writer": { "directly_related_user_types": [ + { + "type": "serviceaccount" + }, + { + "type": "serviceaccount", + "wildcard": {} + }, { "type": "user" }, @@ -416,6 +472,40 @@ }, { "type": "user" + }, + { + "metadata": { + "relations": { + "administator": { + "directly_related_user_types": [ + { + "type": "serviceaccount" + }, + { + "type": "serviceaccount", + "wildcard": {} + }, + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "relation": "member", + "type": "group" + } + ] + } + } + }, + "relations": { + "administator": { + "this": {} + } + }, + "type": "serviceaccount" } ] -} \ No newline at end of file +} diff --git a/pkg/names/names.go b/pkg/names/names.go index b50d6ab78..30631ac60 100644 --- a/pkg/names/names.go +++ b/pkg/names/names.go @@ -45,6 +45,11 @@ func ParseTag(tag string) (names.Tag, error) { return nil, invalidTagError(tag, kind) } return NewApplicationOfferTag(id), nil + case ServiceAccountTagKind: + if !IsValidServiceAccountId(id) { + return nil, invalidTagError(tag, kind) + } + return NewServiceAccountTag(id), nil default: return names.ParseTag(tag) } diff --git a/pkg/names/service_account.go b/pkg/names/service_account.go new file mode 100644 index 000000000..4e851b38d --- /dev/null +++ b/pkg/names/service_account.go @@ -0,0 +1,66 @@ +// Copyright 2024 canonical. + +// Service accounts are an OIDC/OAuth concept which allows for machine<->machine communication. +// Service accounts are identified by their client ID. +package names + +import ( + "fmt" + "regexp" +) + +const ( + // ServiceAccountTagKind represents the resource "kind" that service accounts + // are represented as. + ServiceAccountTagKind = "serviceaccount" +) + +var ( + validClientIdSnippet = `^[0-9a-zA-Z-]+$` + validClientId = regexp.MustCompile(validClientIdSnippet) +) + +// ServiceAccount represents a service account where id is the client ID. +// Implements juju names.Tag. +type ServiceAccountTag struct { + id string +} + +// Id implements juju names.Tag. +func (t ServiceAccountTag) Id() string { return t.id } + +// Kind implements juju names.Tag. +func (t ServiceAccountTag) Kind() string { return ServiceAccountTagKind } + +// String implements juju names.Tag. +func (t ServiceAccountTag) String() string { return ServiceAccountTagKind + "-" + t.Id() } + +// NewServiceAccountTag creates a valid ServiceAccountTag if it is possible to parse +// the provided tag. +func NewServiceAccountTag(clientId string) ServiceAccountTag { + id := validClientId.FindString(clientId) + + if id == "" { + panic(fmt.Sprintf("invalid client tag %q", clientId)) + } + + return ServiceAccountTag{id: id} +} + +// ParseServiceAccountTag parses a service account tag string. +func ParseServiceAccountTag(tag string) (ServiceAccountTag, error) { + t, err := ParseTag(tag) + if err != nil { + return ServiceAccountTag{}, err + } + gt, ok := t.(ServiceAccountTag) + if !ok { + return ServiceAccountTag{}, invalidTagError(tag, ServiceAccountTagKind) + } + return gt, nil +} + +// IsValidServiceAccountId verifies the client id for a service account is valid according to a regex internally. +func IsValidServiceAccountId(id string) bool { + return validClientId.MatchString(id) +} diff --git a/pkg/names/service_account_test.go b/pkg/names/service_account_test.go new file mode 100644 index 000000000..2f2b29434 --- /dev/null +++ b/pkg/names/service_account_test.go @@ -0,0 +1,57 @@ +package names + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseServiceAccountID(t *testing.T) { + tests := []struct { + about string + tag string + expectedID string + err string + }{{ + about: "Valid svc account tag", + tag: "serviceaccount-1e654457-a195-4a41-8360-929c7f455d43", + expectedID: "1e654457-a195-4a41-8360-929c7f455d43", + err: "", + }, { + about: "Invalid svc account tag (serviceaccounts)", + tag: "serviceaccounts-1e654457-a195-4a41-8360-929c7f455d43", + err: "is not a valid tag", + }, { + about: "Invalid svc account tag (no prefix)", + tag: "1e654457-a195-4a41-8360-929c7f455d43", + err: "is not a valid tag", + }, { + about: "Invalid svc account tag (missing ID)", + tag: "serviceaccounts-", + err: "is not a valid tag", + }} + for _, test := range tests { + t.Run(test.about, func(t *testing.T) { + gt, err := ParseServiceAccountTag(test.tag) + if test.err == "" { + assert.NoError(t, err) + assert.Equal(t, test.expectedID, gt.id) + assert.Equal(t, test.expectedID, gt.Id()) + assert.Equal(t, "serviceaccount", gt.Kind()) + assert.Equal(t, test.tag, gt.String()) + } else { + assert.ErrorContains(t, err, test.err) + } + }) + } +} + +func TestIsValidServiceAccountId(t *testing.T) { + assert.True(t, IsValidServiceAccountId("1e654457-a195-4a41-8360-929c7f455d43")) + assert.True(t, IsValidServiceAccountId("12345")) + assert.True(t, IsValidServiceAccountId("abc123")) + assert.True(t, IsValidServiceAccountId("ABC123")) + assert.False(t, IsValidServiceAccountId("abc 123")) + assert.False(t, IsValidServiceAccountId("")) + assert.False(t, IsValidServiceAccountId(" ")) +} From 3ac8ea78f7f70d5d8dd4bf4f0bd0796e4b497ad1 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Jan 2024 13:38:39 +0000 Subject: [PATCH 004/126] Upgrade dbmodel version to v1.6 Signed-off-by: Babak K. Shandiz --- internal/dbmodel/sql/postgres/1_6.sql | 27 +++++++++++++++++++++++++++ internal/dbmodel/version.go | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 internal/dbmodel/sql/postgres/1_6.sql diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql new file mode 100644 index 000000000..905290acc --- /dev/null +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -0,0 +1,27 @@ +-- 1_6.sql is a migration that renames `user` to `identity`. + +-- Note that we don't need to rename underlying indexes/constraints. As Postgres +-- docs states: +-- "When renaming a constraint that has an underlying index, the index is renamed as well." +-- (See https://www.postgresql.org/docs/current/sql-altertable.html) + +ALTER TABLE IF EXISTS users RENAME TO identities; +ALTER TABLE IF EXISTS users RENAME COLUMN username TO identity_name; + +ALTER TABLE IF EXISTS cloud_credentials RENAME COLUMN owner_username TO owner_identity_name; +ALTER TABLE IF EXISTS cloud_defaults RENAME COLUMN username TO identity_name; +ALTER TABLE IF EXISTS models RENAME COLUMN owner_username TO owner_identity_name; +ALTER TABLE IF EXISTS application_offer_connections RENAME COLUMN username TO identity_name; +ALTER TABLE IF EXISTS user_model_defaults RENAME COLUMN username TO identity_name; + +-- TODO (babakks): Do we need to rename these two instances as well? +ALTER TABLE IF EXISTS controllers RENAME COLUMN admin_user TO admin_identity_name; +ALTER TABLE IF EXISTS audit_log RENAME COLUMN user_tag TO identity_tag; + +-- We don't need to rename columns in these tables, because they're already +-- dropped in an earlier migration: +-- - user_application_offer_access +-- - user_cloud_access +-- - user_model_access + +UPDATE versions SET major=1, minor=6 WHERE component='jimmdb'; diff --git a/internal/dbmodel/version.go b/internal/dbmodel/version.go index 5c3ee485c..56730709f 100644 --- a/internal/dbmodel/version.go +++ b/internal/dbmodel/version.go @@ -20,7 +20,7 @@ const ( // Minor is the minor version of the model described in the dbmodel // package. It should be incremented for any change made to the // database model from database model in a released JIMM. - Minor = 5 + Minor = 6 ) type Version struct { From 761f09eb912e1fa87efd72022b0f77312315cd8c Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Jan 2024 13:57:38 +0000 Subject: [PATCH 005/126] Rename `User` type `Identity` Signed-off-by: Babak K. Shandiz --- cmd/jimmctl/cmd/addcloudtocontroller_test.go | 4 +- cmd/jimmctl/cmd/jimmsuite_test.go | 10 +- cmd/jimmctl/cmd/relation_test.go | 10 +- discharger.go | 2 +- internal/auth/jujuauth.go | 2 +- internal/auth/jujuauth_test.go | 6 +- internal/db/applicationoffer_test.go | 6 +- internal/db/cloudcredential_test.go | 8 +- internal/db/clouddefaults.go | 2 +- internal/db/clouddefaults_test.go | 4 +- internal/db/controller_test.go | 2 +- internal/db/db_test.go | 2 +- internal/db/model_test.go | 12 +- internal/db/user.go | 8 +- internal/db/user_test.go | 28 +-- internal/db/usermodeldefaults_test.go | 10 +- internal/dbmodel/cloudcredential.go | 2 +- internal/dbmodel/cloudcredential_test.go | 4 +- internal/dbmodel/clouddefaults.go | 2 +- internal/dbmodel/controller_test.go | 2 +- internal/dbmodel/model.go | 4 +- internal/dbmodel/model_test.go | 6 +- internal/dbmodel/user.go | 12 +- internal/dbmodel/user_test.go | 18 +- internal/dbmodel/usermodeldefaults.go | 2 +- internal/jimm/access.go | 4 +- internal/jimm/access_test.go | 6 +- internal/jimm/applicationoffer.go | 20 +-- internal/jimm/applicationoffer_test.go | 176 +++++++++---------- internal/jimm/cloud.go | 8 +- internal/jimm/cloud_test.go | 20 +-- internal/jimm/cloudcredential.go | 6 +- internal/jimm/cloudcredential_test.go | 68 +++---- internal/jimm/clouddefaults.go | 6 +- internal/jimm/clouddefaults_test.go | 24 +-- internal/jimm/controller.go | 8 +- internal/jimm/controller_test.go | 26 +-- internal/jimm/export_test.go | 2 +- internal/jimm/jimm_test.go | 18 +- internal/jimm/model.go | 12 +- internal/jimm/model_test.go | 14 +- internal/jimm/user.go | 2 +- internal/jimm/user_test.go | 6 +- internal/jimm/usermodeldefaults.go | 4 +- internal/jimm/usermodeldefaults_test.go | 14 +- internal/jimm/watcher_test.go | 4 +- internal/jimmtest/cmp.go | 2 +- internal/jimmtest/env.go | 6 +- internal/jimmtest/jimm_mock.go | 32 ++-- internal/jimmtest/suite.go | 10 +- internal/jujuapi/access_control_test.go | 4 +- internal/jujuapi/applicationoffers_test.go | 2 +- internal/jujuapi/cloud.go | 6 +- internal/jujuapi/cloud_test.go | 4 +- internal/jujuapi/controller.go | 2 +- internal/jujuapi/controllerroot.go | 18 +- internal/jujuapi/jimm_test.go | 6 +- internal/jujuapi/modelmanager.go | 6 +- internal/jujuapi/modelmanager_test.go | 4 +- internal/jujuapi/websocket_test.go | 4 +- internal/openfga/user.go | 10 +- internal/openfga/user_test.go | 48 ++--- local/seed_db/main.go | 2 +- service.go | 2 +- service_test.go | 12 +- 65 files changed, 398 insertions(+), 398 deletions(-) diff --git a/cmd/jimmctl/cmd/addcloudtocontroller_test.go b/cmd/jimmctl/cmd/addcloudtocontroller_test.go index c347cda4d..602983535 100644 --- a/cmd/jimmctl/cmd/addcloudtocontroller_test.go +++ b/cmd/jimmctl/cmd/addcloudtocontroller_test.go @@ -31,7 +31,7 @@ func (s *addCloudToControllerSuite) SetUpTest(c *gc.C) { s.jimmSuite.SetUpTest(c) // We add user bob, who is a JIMM administrator. - err := s.JIMM.Database.UpdateUser(context.Background(), &dbmodel.User{ + err := s.JIMM.Database.UpdateUser(context.Background(), &dbmodel.Identity{ DisplayName: "Bob", Username: "bob@external", }) @@ -51,7 +51,7 @@ func (s *addCloudToControllerSuite) SetUpTest(c *gc.C) { // We grant user bob administrator access to JIMM and the added // test-cloud. bob := openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: "bob@external", }, s.JIMM.OpenFGAClient, diff --git a/cmd/jimmctl/cmd/jimmsuite_test.go b/cmd/jimmctl/cmd/jimmsuite_test.go index f3fc0c813..c2918da6b 100644 --- a/cmd/jimmctl/cmd/jimmsuite_test.go +++ b/cmd/jimmctl/cmd/jimmsuite_test.go @@ -39,7 +39,7 @@ type jimmSuite struct { Params service.Params HTTP *httptest.Server Service *service.Service - AdminUser *dbmodel.User + AdminUser *dbmodel.Identity ClientStore func() *jjclient.MemStore JIMM *jimm.JIMM cancel context.CancelFunc @@ -103,7 +103,7 @@ func (s *jimmSuite) SetUpTest(c *gc.C) { s.ControllerAdmins = []string{"controller-admin"} s.JujuConnSuite.SetUpTest(c) - s.AdminUser = &dbmodel.User{ + s.AdminUser = &dbmodel.Identity{ Username: "alice@external", LastLogin: db.Now(), } @@ -207,7 +207,7 @@ func (s *jimmSuite) AddController(c *gc.C, name string, info *api.Info) { func (s *jimmSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, cred jujuparams.CloudCredential) { ctx := context.Background() - u := dbmodel.User{ + u := dbmodel.Identity{ Username: tag.Owner().Id(), } user := openfga.NewUser(&u, s.JIMM.OpenFGAClient) @@ -224,12 +224,12 @@ func (s *jimmSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, func (s *jimmSuite) AddModel(c *gc.C, owner names.UserTag, name string, cloud names.CloudTag, region string, cred names.CloudCredentialTag) names.ModelTag { ctx := context.Background() u := openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: owner.Id(), }, s.OFGAClient, ) - err := s.JIMM.Database.GetUser(ctx, u.User) + err := s.JIMM.Database.GetUser(ctx, u.Identity) c.Assert(err, gc.Equals, nil) mi, err := s.JIMM.AddModel(ctx, u, &jimm.ModelCreateArgs{ Name: name, diff --git a/cmd/jimmctl/cmd/relation_test.go b/cmd/jimmctl/cmd/relation_test.go index 1ace9b81f..a5c24c80e 100644 --- a/cmd/jimmctl/cmd/relation_test.go +++ b/cmd/jimmctl/cmd/relation_test.go @@ -255,7 +255,7 @@ func (s *relationSuite) TestRemoveRelation(c *gc.C) { } type environment struct { - users []dbmodel.User + users []dbmodel.Identity clouds []dbmodel.Cloud credentials []dbmodel.CloudCredential controllers []dbmodel.Controller @@ -263,15 +263,15 @@ type environment struct { applicationOffers []dbmodel.ApplicationOffer } -func initializeEnvironment(c *gc.C, ctx context.Context, db *db.Database, u dbmodel.User) *environment { +func initializeEnvironment(c *gc.C, ctx context.Context, db *db.Database, u dbmodel.Identity) *environment { env := environment{} - u1 := dbmodel.User{ + u1 := dbmodel.Identity{ Username: "eve@external", } c.Assert(db.DB.Create(&u1).Error, gc.IsNil) - env.users = []dbmodel.User{u, u1} + env.users = []dbmodel.Identity{u, u1} cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -448,7 +448,7 @@ func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { err = db.GetGroup(ctx, &group) c.Assert(err, gc.IsNil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: petname.Generate(2, "-") + "@external", } c.Assert(db.DB.Create(&u).Error, gc.IsNil) diff --git a/discharger.go b/discharger.go index de5de78ca..e6bf6706d 100644 --- a/discharger.go +++ b/discharger.go @@ -106,7 +106,7 @@ func (md *macaroonDischarger) checkThirdPartyCaveat(ctx context.Context, req *ht offerTag := jimmnames.NewApplicationOfferTag(offerUUID) user := openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: userTag.Id(), }, md.ofgaClient, diff --git a/internal/auth/jujuauth.go b/internal/auth/jujuauth.go index e58f201ba..619938927 100644 --- a/internal/auth/jujuauth.go +++ b/internal/auth/jujuauth.go @@ -71,7 +71,7 @@ func (a JujuAuthenticator) Authenticate(ctx context.Context, req *jujuparams.Log if ut.IsLocal() { ut = ut.WithDomain("external") } - u := &dbmodel.User{ + u := &dbmodel.Identity{ Username: ut.Id(), DisplayName: ut.Name(), } diff --git a/internal/auth/jujuauth_test.go b/internal/auth/jujuauth_test.go index 98abdc701..109888a80 100644 --- a/internal/auth/jujuauth_test.go +++ b/internal/auth/jujuauth_test.go @@ -59,7 +59,7 @@ func TestAuthenticateLogin(t *testing.T) { c.Assert(err, qt.IsNil) c.Check(u.LastLogin.Valid, qt.Equals, false) u.LastLogin = sql.NullTime{} - c.Check(u.User, qt.DeepEquals, &dbmodel.User{ + c.Check(u.Identity, qt.DeepEquals, &dbmodel.Identity{ Username: "alice@external", DisplayName: "alice", }) @@ -102,7 +102,7 @@ func TestAuthenticateLoginWithDomain(t *testing.T) { c.Assert(err, qt.IsNil) c.Check(u.LastLogin.Valid, qt.Equals, false) u.LastLogin = sql.NullTime{} - c.Check(u.User, qt.DeepEquals, &dbmodel.User{ + c.Check(u.Identity, qt.DeepEquals, &dbmodel.Identity{ Username: "alice@mydomain", DisplayName: "alice", }) @@ -146,7 +146,7 @@ func TestAuthenticateLoginSuperuser(t *testing.T) { c.Assert(err, qt.IsNil) c.Check(u.LastLogin.Valid, qt.Equals, false) u.LastLogin = sql.NullTime{} - c.Check(u.User, qt.DeepEquals, &dbmodel.User{ + c.Check(u.Identity, qt.DeepEquals, &dbmodel.Identity{ Username: "bob@external", DisplayName: "bob", }) diff --git a/internal/db/applicationoffer_test.go b/internal/db/applicationoffer_test.go index d70603a0d..1ec3485c7 100644 --- a/internal/db/applicationoffer_test.go +++ b/internal/db/applicationoffer_test.go @@ -27,7 +27,7 @@ func TestAddApplicationOfferUnconfiguredDatabase(t *testing.T) { } type testEnvironment struct { - u dbmodel.User + u dbmodel.Identity cloud dbmodel.Cloud cred dbmodel.CloudCredential controller dbmodel.Controller @@ -40,7 +40,7 @@ func initTestEnvironment(c *qt.C, db *db.Database) testEnvironment { env := testEnvironment{} - env.u = dbmodel.User{ + env.u = dbmodel.Identity{ Username: "bob@external", } c.Assert(db.DB.Create(&env.u).Error, qt.IsNil) @@ -241,7 +241,7 @@ func (s *dbSuite) TestFindApplicationOffers(c *qt.C) { err := s.Database.AddApplicationOffer(context.Background(), &offer1) c.Assert(err, qt.Equals, nil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) diff --git a/internal/db/cloudcredential_test.go b/internal/db/cloudcredential_test.go index 98434a335..f2d55bc56 100644 --- a/internal/db/cloudcredential_test.go +++ b/internal/db/cloudcredential_test.go @@ -30,7 +30,7 @@ func (s *dbSuite) TestSetCloudCredentialInvalidTag(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -58,7 +58,7 @@ func (s *dbSuite) TestSetCloudCredential(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -95,7 +95,7 @@ func (s *dbSuite) TestSetCloudCredentialUpdate(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -165,7 +165,7 @@ func (s *dbSuite) TestGetCloudCredential(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) diff --git a/internal/db/clouddefaults.go b/internal/db/clouddefaults.go index eb31bf026..8d7ded083 100644 --- a/internal/db/clouddefaults.go +++ b/internal/db/clouddefaults.go @@ -135,7 +135,7 @@ func (d *Database) CloudDefaults(ctx context.Context, defaults *dbmodel.CloudDef } // ModelDefaultsForCloud returns the default config values for the specified cloud. -func (d *Database) ModelDefaultsForCloud(ctx context.Context, user *dbmodel.User, cloud names.CloudTag) ([]dbmodel.CloudDefaults, error) { +func (d *Database) ModelDefaultsForCloud(ctx context.Context, user *dbmodel.Identity, cloud names.CloudTag) ([]dbmodel.CloudDefaults, error) { const op = errors.Op("db.ModelDefaultsForCloud") if err := d.ready(); err != nil { diff --git a/internal/db/clouddefaults_test.go b/internal/db/clouddefaults_test.go index eac5058ee..368bb4365 100644 --- a/internal/db/clouddefaults_test.go +++ b/internal/db/clouddefaults_test.go @@ -22,7 +22,7 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { err := s.Database.Migrate(ctx, true) c.Assert(err, qt.Equals, nil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -155,7 +155,7 @@ func TestModelDefaultsForCloudUnconfiguredDatabase(t *testing.T) { c := qt.New(t) var d db.Database - _, err := d.ModelDefaultsForCloud(context.Background(), &dbmodel.User{}, names.NewCloudTag("test-cloud")) + _, err := d.ModelDefaultsForCloud(context.Background(), &dbmodel.Identity{}, names.NewCloudTag("test-cloud")) c.Check(err, qt.ErrorMatches, `database not configured`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeServerConfiguration) } diff --git a/internal/db/controller_test.go b/internal/db/controller_test.go index 3466c53e2..17e54ce1e 100644 --- a/internal/db/controller_test.go +++ b/internal/db/controller_test.go @@ -122,7 +122,7 @@ func (s *dbSuite) TestGetControllerWithModels(c *qt.C) { CloudName: "test-cloud", CloudRegion: "test-region", } - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) diff --git a/internal/db/db_test.go b/internal/db/db_test.go index 9d6777b78..bd714d095 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -62,7 +62,7 @@ func (s *dbSuite) TestTransaction(c *qt.C) { err = s.Database.Transaction(func(d *db.Database) error { c.Check(d, qt.Not(qt.Equals), s.Database) - return d.GetUser(context.Background(), &dbmodel.User{Username: "bob@external"}) + return d.GetUser(context.Background(), &dbmodel.Identity{Username: "bob@external"}) }) c.Assert(err, qt.IsNil) diff --git a/internal/db/model_test.go b/internal/db/model_test.go index 89cf1a7d5..aa573f2b2 100644 --- a/internal/db/model_test.go +++ b/internal/db/model_test.go @@ -31,7 +31,7 @@ func (s *dbSuite) TestAddModel(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -104,7 +104,7 @@ func (s *dbSuite) TestGetModel(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -162,7 +162,7 @@ func (s *dbSuite) TestGetModel(c *qt.C) { }, } model.CloudCredential.Cloud = dbmodel.Cloud{} - model.CloudCredential.Owner = dbmodel.User{} + model.CloudCredential.Owner = dbmodel.Identity{} err = s.Database.AddModel(context.Background(), &model) c.Assert(err, qt.Equals, nil) @@ -204,7 +204,7 @@ func (s *dbSuite) TestUpdateModel(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -282,7 +282,7 @@ func (s *dbSuite) TestDeleteModel(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -356,7 +356,7 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) diff --git a/internal/db/user.go b/internal/db/user.go index ae074d3a4..230fc3cee 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -19,7 +19,7 @@ import ( // this information. // // GetUser returns an error with CodeNotFound if the username is invalid. -func (d *Database) GetUser(ctx context.Context, u *dbmodel.User) error { +func (d *Database) GetUser(ctx context.Context, u *dbmodel.Identity) error { const op = errors.Op("db.GetUser") if err := d.ready(); err != nil { return errors.E(op, err) @@ -40,7 +40,7 @@ func (d *Database) GetUser(ctx context.Context, u *dbmodel.User) error { // will not create a user if the user cannot be found. // // FetchUser returns an error with CodeNotFound if the username is invalid. -func (d *Database) FetchUser(ctx context.Context, u *dbmodel.User) error { +func (d *Database) FetchUser(ctx context.Context, u *dbmodel.Identity) error { const op = errors.Op("db.FetchUser") if err := d.ready(); err != nil { return errors.E(op, err) @@ -63,7 +63,7 @@ func (d *Database) FetchUser(ctx context.Context, u *dbmodel.User) error { // // UpdateUser returns an error with CodeNotFound if the username is // invalid. -func (d *Database) UpdateUser(ctx context.Context, u *dbmodel.User) error { +func (d *Database) UpdateUser(ctx context.Context, u *dbmodel.Identity) error { const op = errors.Op("db.UpdateUser") if err := d.ready(); err != nil { return errors.E(op, err) @@ -82,7 +82,7 @@ func (d *Database) UpdateUser(ctx context.Context, u *dbmodel.User) error { } // GetUserCloudCredentials fetches user cloud credentials for the specified cloud. -func (d *Database) GetUserCloudCredentials(ctx context.Context, u *dbmodel.User, cloud string) ([]dbmodel.CloudCredential, error) { +func (d *Database) GetUserCloudCredentials(ctx context.Context, u *dbmodel.Identity, cloud string) ([]dbmodel.CloudCredential, error) { const op = errors.Op("db.GetUserCloudCredentials") if err := d.ready(); err != nil { return nil, errors.E(op, err) diff --git a/internal/db/user_test.go b/internal/db/user_test.go index 4dce0925f..8c47372db 100644 --- a/internal/db/user_test.go +++ b/internal/db/user_test.go @@ -17,31 +17,31 @@ func TestGetUserUnconfiguredDatabase(t *testing.T) { c := qt.New(t) var d db.Database - err := d.GetUser(context.Background(), &dbmodel.User{}) + err := d.GetUser(context.Background(), &dbmodel.Identity{}) c.Check(err, qt.ErrorMatches, `database not configured`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeServerConfiguration) } func (s *dbSuite) TestGetUser(c *qt.C) { ctx := context.Background() - err := s.Database.GetUser(ctx, &dbmodel.User{}) + err := s.Database.GetUser(ctx, &dbmodel.Identity{}) c.Check(err, qt.ErrorMatches, `upgrade in progress`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeUpgradeInProgress) err = s.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - err = s.Database.GetUser(ctx, &dbmodel.User{}) + err = s.Database.GetUser(ctx, &dbmodel.Identity{}) c.Check(err, qt.ErrorMatches, `invalid username ""`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } err = s.Database.GetUser(ctx, &u) c.Assert(err, qt.IsNil) - u2 := dbmodel.User{ + u2 := dbmodel.Identity{ Username: u.Username, } err = s.Database.GetUser(ctx, &u2) @@ -53,25 +53,25 @@ func TestUpdateUserUnconfiguredDatabase(t *testing.T) { c := qt.New(t) var d db.Database - err := d.UpdateUser(context.Background(), &dbmodel.User{}) + err := d.UpdateUser(context.Background(), &dbmodel.Identity{}) c.Check(err, qt.ErrorMatches, `database not configured`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeServerConfiguration) } func (s *dbSuite) TestUpdateUser(c *qt.C) { ctx := context.Background() - err := s.Database.UpdateUser(ctx, &dbmodel.User{}) + err := s.Database.UpdateUser(ctx, &dbmodel.Identity{}) c.Check(err, qt.ErrorMatches, `upgrade in progress`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeUpgradeInProgress) err = s.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - err = s.Database.UpdateUser(ctx, &dbmodel.User{}) + err = s.Database.UpdateUser(ctx, &dbmodel.Identity{}) c.Check(err, qt.ErrorMatches, `invalid username ""`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } err = s.Database.GetUser(ctx, &u) @@ -80,7 +80,7 @@ func (s *dbSuite) TestUpdateUser(c *qt.C) { err = s.Database.UpdateUser(ctx, &u) c.Assert(err, qt.IsNil) - u2 := dbmodel.User{ + u2 := dbmodel.Identity{ Username: u.Username, } err = s.Database.GetUser(ctx, &u2) @@ -92,7 +92,7 @@ func TestGetUserCloudCredentialsUnconfiguredDatabase(t *testing.T) { c := qt.New(t) var d db.Database - _, err := d.GetUserCloudCredentials(context.Background(), &dbmodel.User{}, "") + _, err := d.GetUserCloudCredentials(context.Background(), &dbmodel.Identity{}, "") c.Check(err, qt.ErrorMatches, `database not configured`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeServerConfiguration) } @@ -103,16 +103,16 @@ func (s *dbSuite) TestGetUserCloudCredentials(c *qt.C) { err := s.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - _, err = s.Database.GetUserCloudCredentials(ctx, &dbmodel.User{}, "") + _, err = s.Database.GetUserCloudCredentials(ctx, &dbmodel.Identity{}, "") c.Check(err, qt.ErrorMatches, `cloudcredential not found`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) - _, err = s.Database.GetUserCloudCredentials(ctx, &dbmodel.User{ + _, err = s.Database.GetUserCloudCredentials(ctx, &dbmodel.Identity{ Username: "test", }, "ec2") c.Check(err, qt.IsNil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) diff --git a/internal/db/usermodeldefaults_test.go b/internal/db/usermodeldefaults_test.go index 0d5f6f25c..421543407 100644 --- a/internal/db/usermodeldefaults_test.go +++ b/internal/db/usermodeldefaults_test.go @@ -24,7 +24,7 @@ func TestSetUserModelDefaults(t *testing.T) { now := time.Now() type testConfig struct { - user *dbmodel.User + user *dbmodel.Identity defaults map[string]interface{} expectedError string expectedDefaults *dbmodel.UserModelDefaults @@ -37,7 +37,7 @@ func TestSetUserModelDefaults(t *testing.T) { }{{ about: "defaults do not exist yet - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -62,7 +62,7 @@ func TestSetUserModelDefaults(t *testing.T) { }, { about: "defaults already exist - defaults updated", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -97,7 +97,7 @@ func TestSetUserModelDefaults(t *testing.T) { }, { about: "user does not exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } @@ -116,7 +116,7 @@ func TestSetUserModelDefaults(t *testing.T) { }, { about: "cannot set agent-version", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) diff --git a/internal/dbmodel/cloudcredential.go b/internal/dbmodel/cloudcredential.go index f8adf20f3..3308d337d 100644 --- a/internal/dbmodel/cloudcredential.go +++ b/internal/dbmodel/cloudcredential.go @@ -23,7 +23,7 @@ type CloudCredential struct { // Owner is the user that owns this credential. OwnerUsername string - Owner User `gorm:"foreignKey:OwnerUsername;references:Username"` + Owner Identity `gorm:"foreignKey:OwnerUsername;references:Username"` // AuthType is the type of the credential. AuthType string diff --git a/internal/dbmodel/cloudcredential_test.go b/internal/dbmodel/cloudcredential_test.go index 804d207f8..d5a0deb53 100644 --- a/internal/dbmodel/cloudcredential_test.go +++ b/internal/dbmodel/cloudcredential_test.go @@ -36,7 +36,7 @@ func TestCloudCredential(t *testing.T) { Cloud: dbmodel.Cloud{ Name: "test-cloud", }, - Owner: dbmodel.User{ + Owner: dbmodel.Identity{ Username: "bob@external", }, AuthType: "empty", @@ -69,7 +69,7 @@ func TestCloudCredentialsCascadeOnDelete(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential", Cloud: cloud, - Owner: dbmodel.User{ + Owner: dbmodel.Identity{ Username: "bob@external", }, } diff --git a/internal/dbmodel/clouddefaults.go b/internal/dbmodel/clouddefaults.go index 54fb7d6a1..926f6bb90 100644 --- a/internal/dbmodel/clouddefaults.go +++ b/internal/dbmodel/clouddefaults.go @@ -9,7 +9,7 @@ type CloudDefaults struct { gorm.Model Username string - User User `gorm:"foreignKey:Username;references:Username"` + User Identity `gorm:"foreignKey:Username;references:Username"` CloudID uint Cloud Cloud diff --git a/internal/dbmodel/controller_test.go b/internal/dbmodel/controller_test.go index 9ca0cae6f..fc1d8056c 100644 --- a/internal/dbmodel/controller_test.go +++ b/internal/dbmodel/controller_test.go @@ -91,7 +91,7 @@ func TestControllerModels(t *testing.T) { } c.Assert(db.Create(&m1).Error, qt.IsNil) - u2 := dbmodel.User{ + u2 := dbmodel.Identity{ Username: "charlie@external", } c.Assert(db.Create(&u2).Error, qt.IsNil) diff --git a/internal/dbmodel/model.go b/internal/dbmodel/model.go index bedd8643f..fb5aa23b4 100644 --- a/internal/dbmodel/model.go +++ b/internal/dbmodel/model.go @@ -31,7 +31,7 @@ type Model struct { // Owner is user that owns the model. OwnerUsername string - Owner User `gorm:"foreignkey:OwnerUsername;references:Username"` + Owner Identity `gorm:"foreignkey:OwnerUsername;references:Username"` // Controller is the controller that is hosting the model. ControllerID uint @@ -103,7 +103,7 @@ func (m *Model) SetTag(t names.ModelTag) { } // FromModelUpdate updates the model from the given ModelUpdate. -func (m *Model) SwitchOwner(u *User) { +func (m *Model) SwitchOwner(u *Identity) { m.OwnerUsername = u.Username m.Owner = *u } diff --git a/internal/dbmodel/model_test.go b/internal/dbmodel/model_test.go index 1e1127f92..e4e42bef9 100644 --- a/internal/dbmodel/model_test.go +++ b/internal/dbmodel/model_test.go @@ -316,8 +316,8 @@ func TestToJujuModelSummary(t *testing.T) { // initModelEnv initialises a controller, cloud and cloud-credential so // that a model can be created. -func initModelEnv(c *qt.C, db *gorm.DB) (dbmodel.Cloud, dbmodel.CloudCredential, dbmodel.Controller, dbmodel.User) { - u := dbmodel.User{ +func initModelEnv(c *qt.C, db *gorm.DB) (dbmodel.Cloud, dbmodel.CloudCredential, dbmodel.Controller, dbmodel.Identity) { + u := dbmodel.Identity{ Username: "bob@external", } c.Assert(db.Create(&u).Error, qt.IsNil) @@ -414,7 +414,7 @@ func TestModelFromJujuModelInfo(t *testing.T) { CloudCredential: dbmodel.CloudCredential{ Name: "test-cred", CloudName: "test-cloud", - Owner: dbmodel.User{ + Owner: dbmodel.Identity{ Username: "bob@external", }, }, diff --git a/internal/dbmodel/user.go b/internal/dbmodel/user.go index 8ade75c92..81421f440 100644 --- a/internal/dbmodel/user.go +++ b/internal/dbmodel/user.go @@ -10,8 +10,8 @@ import ( "gorm.io/gorm" ) -// A User represents a JIMM user. -type User struct { +// A Identity represents a JIMM user. +type Identity struct { gorm.Model // Username is the username for the user. This is the juju @@ -37,7 +37,7 @@ type User struct { } // Tag returns a names.Tag for the user. -func (u User) Tag() names.Tag { +func (u Identity) Tag() names.Tag { return u.ResourceTag() } @@ -45,17 +45,17 @@ func (u User) Tag() names.Tag { // is intended to be used in places where we expect to see // a concrete type names.UserTag instead of the // names.Tag interface. -func (u User) ResourceTag() names.UserTag { +func (u Identity) ResourceTag() names.UserTag { return names.NewUserTag(u.Username) } // SetTag sets the username of the user to the value from the given tag. -func (u *User) SetTag(t names.UserTag) { +func (u *Identity) SetTag(t names.UserTag) { u.Username = t.Id() } // ToJujuUserInfo converts a User into a juju UserInfo value. -func (u User) ToJujuUserInfo() jujuparams.UserInfo { +func (u Identity) ToJujuUserInfo() jujuparams.UserInfo { var ui jujuparams.UserInfo ui.Username = u.Username ui.DisplayName = u.DisplayName diff --git a/internal/dbmodel/user_test.go b/internal/dbmodel/user_test.go index d953e0099..24fdc824a 100644 --- a/internal/dbmodel/user_test.go +++ b/internal/dbmodel/user_test.go @@ -19,11 +19,11 @@ func TestUser(t *testing.T) { c := qt.New(t) db := gormDB(c) - var u0 dbmodel.User + var u0 dbmodel.Identity result := db.Where("username = ?", "bob@external").First(&u0) c.Check(result.Error, qt.Equals, gorm.ErrRecordNotFound) - u1 := dbmodel.User{ + u1 := dbmodel.Identity{ Username: "bob@external", DisplayName: "bob", } @@ -31,7 +31,7 @@ func TestUser(t *testing.T) { c.Assert(result.Error, qt.IsNil) c.Check(result.RowsAffected, qt.Equals, int64(1)) - var u2 dbmodel.User + var u2 dbmodel.Identity result = db.Where("username = ?", "bob@external").First(&u2) c.Assert(result.Error, qt.IsNil) c.Check(u2, qt.DeepEquals, u1) @@ -40,12 +40,12 @@ func TestUser(t *testing.T) { u2.LastLogin.Valid = true result = db.Save(&u2) c.Assert(result.Error, qt.IsNil) - var u3 dbmodel.User + var u3 dbmodel.Identity result = db.Where("username = ?", "bob@external").First(&u3) c.Assert(result.Error, qt.IsNil) c.Check(u3, qt.DeepEquals, u2) - u4 := dbmodel.User{ + u4 := dbmodel.Identity{ Username: "bob@external", DisplayName: "bob", } @@ -56,12 +56,12 @@ func TestUser(t *testing.T) { func TestUserTag(t *testing.T) { c := qt.New(t) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } tag := u.Tag() c.Check(tag.String(), qt.Equals, "user-bob@external") - var u2 dbmodel.User + var u2 dbmodel.Identity u2.SetTag(tag.(names.UserTag)) c.Check(u2, qt.DeepEquals, u) } @@ -76,7 +76,7 @@ func TestUserCloudCredentials(t *testing.T) { result := db.Create(&cl) c.Assert(result.Error, qt.IsNil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "bob@external", } result = db.Create(&u) @@ -121,7 +121,7 @@ func TestUserCloudCredentials(t *testing.T) { func TestUserToJujuUserInfo(t *testing.T) { c := qt.New(t) - u := dbmodel.User{ + u := dbmodel.Identity{ Model: gorm.Model{ CreatedAt: time.Now(), }, diff --git a/internal/dbmodel/usermodeldefaults.go b/internal/dbmodel/usermodeldefaults.go index a7d0b9214..28c9743e9 100644 --- a/internal/dbmodel/usermodeldefaults.go +++ b/internal/dbmodel/usermodeldefaults.go @@ -9,7 +9,7 @@ type UserModelDefaults struct { gorm.Model Username string - User User `gorm:"foreignKey:Username;references:Username"` + User Identity `gorm:"foreignKey:Username;references:Username"` Defaults Map } diff --git a/internal/jimm/access.go b/internal/jimm/access.go index e1422852d..148c04098 100644 --- a/internal/jimm/access.go +++ b/internal/jimm/access.go @@ -317,7 +317,7 @@ func (j *JIMM) GrantAuditLogAccess(ctx context.Context, user *openfga.User, targ return errors.E(op, errors.CodeUnauthorized, "unauthorized") } - targetUser := &dbmodel.User{} + targetUser := &dbmodel.Identity{} targetUser.SetTag(targetUserTag) err := j.Database.GetUser(ctx, targetUser) if err != nil { @@ -340,7 +340,7 @@ func (j *JIMM) RevokeAuditLogAccess(ctx context.Context, user *openfga.User, tar return errors.E(op, errors.CodeUnauthorized, "unauthorized") } - targetUser := &dbmodel.User{} + targetUser := &dbmodel.Identity{} targetUser.SetTag(targetUserTag) err := j.Database.GetUser(ctx, targetUser) if err != nil { diff --git a/internal/jimm/access_test.go b/internal/jimm/access_test.go index 5bea4814d..a79eebce0 100644 --- a/internal/jimm/access_test.go +++ b/internal/jimm/access_test.go @@ -34,7 +34,7 @@ func (ta *testAuthenticator) Authenticate(ctx context.Context, req *jujuparams.L return nil, ta.err } return &openfga.User{ - User: &dbmodel.User{ + Identity: &dbmodel.Identity{ Username: ta.username, }, }, nil @@ -142,11 +142,11 @@ func TestAuditLogAccess(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - adminUser := openfga.NewUser(&dbmodel.User{Username: "alice"}, j.OpenFGAClient) + adminUser := openfga.NewUser(&dbmodel.Identity{Username: "alice"}, j.OpenFGAClient) err = adminUser.SetControllerAccess(ctx, j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) - user := openfga.NewUser(&dbmodel.User{Username: "bob"}, j.OpenFGAClient) + user := openfga.NewUser(&dbmodel.Identity{Username: "bob"}, j.OpenFGAClient) // admin user can grant other users audit log access. err = j.GrantAuditLogAccess(ctx, adminUser, user.ResourceTag()) diff --git a/internal/jimm/applicationoffer.go b/internal/jimm/applicationoffer.go index 294369d53..f56d165dd 100644 --- a/internal/jimm/applicationoffer.go +++ b/internal/jimm/applicationoffer.go @@ -141,7 +141,7 @@ func (j *JIMM) Offer(ctx context.Context, user *openfga.User, offer AddApplicati ownerId = user.Tag().Id() } owner := openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: ownerId, }, j.OpenFGAClient, @@ -155,7 +155,7 @@ func (j *JIMM) Offer(ctx context.Context, user *openfga.User, offer AddApplicati } everyone := openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: ofganames.EveryoneUser, }, j.OpenFGAClient, @@ -223,7 +223,7 @@ func (j *JIMM) GetApplicationOfferConsumeDetails(ctx context.Context, user *open // Fix the consume details from the controller to be correct for JAAS. // Filter out any juju local users. - users, err := j.listApplicationOfferUsers(ctx, offer.ResourceTag(), user.User, accessLevel) + users, err := j.listApplicationOfferUsers(ctx, offer.ResourceTag(), user.Identity, accessLevel) if err != nil { return errors.E(op, err) } @@ -251,7 +251,7 @@ func (j *JIMM) GetApplicationOfferConsumeDetails(ctx context.Context, user *open // only see themselves. // TODO(Kian) CSS-6040 Consider changing wherever this function is used to // better encapsulate transforming Postgres/OpenFGA objects into Juju objects. -func (j *JIMM) listApplicationOfferUsers(ctx context.Context, offer names.ApplicationOfferTag, user *dbmodel.User, accessLevel string) ([]jujuparams.OfferUserDetails, error) { +func (j *JIMM) listApplicationOfferUsers(ctx context.Context, offer names.ApplicationOfferTag, user *dbmodel.Identity, accessLevel string) ([]jujuparams.OfferUserDetails, error) { users := make(map[string]string) // we loop through relations in a decreasing order of access for _, relation := range []openfga.Relation{ @@ -344,7 +344,7 @@ func (j *JIMM) GetApplicationOffer(ctx context.Context, user *openfga.User, offe if accessLevel != string(jujuparams.OfferAdminAccess) { offerDetails.Connections = nil } - users, err := j.listApplicationOfferUsers(ctx, offer.ResourceTag(), user.User, accessLevel) + users, err := j.listApplicationOfferUsers(ctx, offer.ResourceTag(), user.Identity, accessLevel) if err != nil { return nil, errors.E(op, err) } @@ -358,7 +358,7 @@ func (j *JIMM) GrantOfferAccess(ctx context.Context, user *openfga.User, offerUR const op = errors.Op("jimm.GrantOfferAccess") err := j.doApplicationOfferAdmin(ctx, user, offerURL, func(offer *dbmodel.ApplicationOffer, api API) error { - tUser := openfga.NewUser(&dbmodel.User{Username: ut.Id()}, j.OpenFGAClient) + tUser := openfga.NewUser(&dbmodel.Identity{Username: ut.Id()}, j.OpenFGAClient) currentRelation := tUser.GetApplicationOfferAccess(ctx, offer.ResourceTag()) currentAccessLevel := ToOfferAccessString(currentRelation) targetAccessLevel := determineAccessLevelAfterGrant(currentAccessLevel, string(access)) @@ -415,7 +415,7 @@ func (j *JIMM) RevokeOfferAccess(ctx context.Context, user *openfga.User, offerU const op = errors.Op("jimm.RevokeOfferAccess") err = j.doApplicationOfferAdmin(ctx, user, offerURL, func(offer *dbmodel.ApplicationOffer, api API) error { - tUser := openfga.NewUser(&dbmodel.User{Username: ut.Id()}, j.OpenFGAClient) + tUser := openfga.NewUser(&dbmodel.Identity{Username: ut.Id()}, j.OpenFGAClient) targetRelation, err := ToOfferRelation(string(access)) if err != nil { return errors.E(op, err) @@ -614,7 +614,7 @@ func (j *JIMM) FindApplicationOffers(ctx context.Context, user *openfga.User, fi if accessLevel != "admin" { offerDetails[i].Connections = nil } - users, err := j.listApplicationOfferUsers(ctx, offer.ResourceTag(), user.User, accessLevel) + users, err := j.listApplicationOfferUsers(ctx, offer.ResourceTag(), user.Identity, accessLevel) if err != nil { return nil, errors.E(op, err) } @@ -664,7 +664,7 @@ func (j *JIMM) applicationOfferFilters(ctx context.Context, jujuFilters ...jujup } if len(f.AllowedConsumerTags) > 0 { for _, u := range f.AllowedConsumerTags { - dbUser := dbmodel.User{ + dbUser := dbmodel.Identity{ Username: u, } ofgaUser := openfga.NewUser(&dbUser, j.OpenFGAClient) @@ -734,7 +734,7 @@ func (j *JIMM) ListApplicationOffers(ctx context.Context, user *openfga.User, fi return nil, errors.E(op, err) } for _, offer := range offerDetails { - users, err := j.listApplicationOfferUsers(ctx, names.NewApplicationOfferTag(offer.OfferUUID), user.User, "admin") + users, err := j.listApplicationOfferUsers(ctx, names.NewApplicationOfferTag(offer.OfferUUID), user.Identity, "admin") if err != nil { return nil, errors.E(op, err) } diff --git a/internal/jimm/applicationoffer_test.go b/internal/jimm/applicationoffer_test.go index f9011bc92..4fe5f35f8 100644 --- a/internal/jimm/applicationoffer_test.go +++ b/internal/jimm/applicationoffer_test.go @@ -31,7 +31,7 @@ import ( ) type environment struct { - users []dbmodel.User + users []dbmodel.Identity clouds []dbmodel.Cloud credentials []dbmodel.CloudCredential controllers []dbmodel.Controller @@ -43,39 +43,39 @@ var initializeEnvironment = func(c *qt.C, ctx context.Context, db *db.Database, env := environment{} // Alice is a model admin, but not a superuser or offer admin. - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) - u1 := dbmodel.User{ + u1 := dbmodel.Identity{ Username: "eve@external", } c.Assert(db.DB.Create(&u1).Error, qt.IsNil) - u2 := dbmodel.User{ + u2 := dbmodel.Identity{ Username: "bob@external", } c.Assert(db.DB.Create(&u2).Error, qt.IsNil) - u3 := dbmodel.User{ + u3 := dbmodel.Identity{ Username: "fred@external", } c.Assert(db.DB.Create(&u3).Error, qt.IsNil) - u4 := dbmodel.User{ + u4 := dbmodel.Identity{ Username: "grant@external", } c.Assert(db.DB.Create(&u4).Error, qt.IsNil) // Jane is an offer admin, but not a superuser or model admin. - u5 := dbmodel.User{ + u5 := dbmodel.Identity{ Username: "jane@external", } c.Assert(db.DB.Create(&u5).Error, qt.IsNil) // Joe is a superuser, but not a model or offer admin. - u6 := dbmodel.User{ + u6 := dbmodel.Identity{ Username: "joe@external", } c.Assert(db.DB.Create(&u6).Error, qt.IsNil) @@ -83,7 +83,7 @@ var initializeEnvironment = func(c *qt.C, ctx context.Context, db *db.Database, err := openfga.NewUser(&u6, client).SetControllerAccess(ctx, names.NewControllerTag(jimmUUID), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) - env.users = []dbmodel.User{u, u1, u2, u3, u4, u5, u6} + env.users = []dbmodel.Identity{u, u1, u2, u3, u4, u5, u6} cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -197,117 +197,117 @@ func TestRevokeOfferAccess(t *testing.T) { tests := []struct { about string - parameterFunc func(*environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) + parameterFunc func(*environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) setup func(*environment, *openfga.OFGAClient) expectedError string expectedAccessLevel string expectedAccessLevelOnError string // This expectation is meant to ensure there'll be no unpredicted behavior (like changing existing relations) after an error has occurred }{{ about: "admin revokes a model's admin user's admin access - an error returns (relation is indirect)", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[1], env.users[0], "test-offer-url", jujuparams.OfferAdminAccess }, expectedError: "failed to unset given access", expectedAccessLevelOnError: "admin", }, { about: "model admin revokes an admin user admin access - user has no access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[1], "test-offer-url", jujuparams.OfferAdminAccess }, expectedAccessLevel: "", }, { about: "admin revokes an admin user admin access - user has no access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[5], env.users[1], "test-offer-url", jujuparams.OfferAdminAccess }, expectedAccessLevel: "", }, { about: "superuser revokes an admin user admin access - user has no access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[6], env.users[1], "test-offer-url", jujuparams.OfferAdminAccess }, expectedAccessLevel: "", }, { about: "admin revokes an admin user consume access - an error returns (no direct relation to remove)", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[1], "test-offer-url", jujuparams.OfferConsumeAccess }, expectedError: "failed to unset given access", expectedAccessLevelOnError: "admin", }, { about: "admin revokes an admin user read access - an error returns (no direct relation to remove)", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[1], "test-offer-url", jujuparams.OfferReadAccess }, expectedError: "failed to unset given access", expectedAccessLevelOnError: "admin", }, { about: "admin revokes a consume user admin access - an error returns (no direct relation to remove)", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[2], "test-offer-url", jujuparams.OfferAdminAccess }, expectedError: "failed to unset given access", expectedAccessLevelOnError: "consume", }, { about: "admin revokes a consume user consume access - user has no access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[2], "test-offer-url", jujuparams.OfferConsumeAccess }, expectedAccessLevel: "", }, { about: "admin revokes a consume user read access - an error returns (no direct relation to remove)", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[2], "test-offer-url", jujuparams.OfferReadAccess }, expectedError: "failed to unset given access", expectedAccessLevelOnError: "consume", }, { about: "admin revokes a read user admin access - an error returns (no direct relation to remove)", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[3], "test-offer-url", jujuparams.OfferAdminAccess }, expectedError: "failed to unset given access", expectedAccessLevelOnError: "read", }, { about: "admin revokes a read user consume access - an error returns (no direct relation to remove)", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[3], "test-offer-url", jujuparams.OfferConsumeAccess }, expectedError: "failed to unset given access", expectedAccessLevelOnError: "read", }, { about: "admin revokes a read user read access - user has no access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[3], "test-offer-url", jujuparams.OfferReadAccess }, expectedAccessLevel: "", }, { about: "admin tries to revoke access to user that does not have access - an error returns", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[4], "test-offer-url", jujuparams.OfferReadAccess }, expectedError: "failed to unset given access", }, { about: "user with consume access cannot revoke access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[2], env.users[3], "test-offer-url", jujuparams.OfferReadAccess }, expectedError: "unauthorized", }, { about: "user with read access cannot revoke access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[3], env.users[3], "test-offer-url", jujuparams.OfferReadAccess }, expectedError: "unauthorized", }, { about: "no such offer", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[3], env.users[3], "no-such-offer", jujuparams.OfferReadAccess }, expectedError: "application offer not found", }, { about: "admin revokes another user (who is direct admin+consumer) their consume access - an error returns (saying user still has access; hinting to use 'jimmctl' for advanced cases)", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[1], env.users[4], env.applicationOffers[0].URL, jujuparams.OfferConsumeAccess }, setup: func(env *environment, client *openfga.OFGAClient) { @@ -318,7 +318,7 @@ func TestRevokeOfferAccess(t *testing.T) { expectedAccessLevelOnError: "admin", }, { about: "admin revokes another user (who is direct admin+reader) their read access - an error returns (saying user still has access; hinting to use 'jimmctl' for advanced cases)", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[1], env.users[4], env.applicationOffers[0].URL, jujuparams.OfferReadAccess }, setup: func(env *environment, client *openfga.OFGAClient) { @@ -329,7 +329,7 @@ func TestRevokeOfferAccess(t *testing.T) { expectedAccessLevelOnError: "admin", }, { about: "admin revokes another user (who is direct consumer+reader) their read access - an error returns (saying user still has access; hinting to use 'jimmctl' for advanced cases)", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[1], env.users[4], env.applicationOffers[0].URL, jujuparams.OfferReadAccess }, setup: func(env *environment, client *openfga.OFGAClient) { @@ -402,108 +402,108 @@ func TestGrantOfferAccess(t *testing.T) { tests := []struct { about string - parameterFunc func(*environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) + parameterFunc func(*environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) expectedError string expectedAccessLevel string }{{ about: "model admin grants an admin user admin access - admin user keeps admin", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[1], "test-offer-url", jujuparams.OfferAdminAccess }, expectedAccessLevel: "admin", }, { about: "model admin grants an admin user consume access - admin user keeps admin", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[1], "test-offer-url", jujuparams.OfferConsumeAccess }, expectedAccessLevel: "admin", }, { about: "model admin grants an admin user read access - admin user keeps admin", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[1], "test-offer-url", jujuparams.OfferReadAccess }, expectedAccessLevel: "admin", }, { about: "model admin grants a consume user admin access - user gets admin access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[2], "test-offer-url", jujuparams.OfferAdminAccess }, expectedAccessLevel: "admin", }, { about: "admin grants a consume user admin access - user gets admin access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[5], env.users[2], "test-offer-url", jujuparams.OfferAdminAccess }, expectedAccessLevel: "admin", }, { about: "superuser grants a consume user admin access - user gets admin access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[6], env.users[2], "test-offer-url", jujuparams.OfferAdminAccess }, expectedAccessLevel: "admin", }, { about: "admin grants a consume user consume access - user keeps consume access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[2], "test-offer-url", jujuparams.OfferConsumeAccess }, expectedAccessLevel: "consume", }, { about: "admin grants a consume user read access - use keeps consume access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[2], "test-offer-url", jujuparams.OfferReadAccess }, expectedAccessLevel: "consume", }, { about: "admin grants a read user admin access - user gets admin access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[3], "test-offer-url", jujuparams.OfferAdminAccess }, expectedAccessLevel: "admin", }, { about: "admin grants a read user consume access - user gets consume access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[3], "test-offer-url", jujuparams.OfferConsumeAccess }, expectedAccessLevel: "consume", }, { about: "admin grants a read user read access - user keeps read access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[3], "test-offer-url", jujuparams.OfferReadAccess }, expectedAccessLevel: "read", }, { about: "no such offer", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[3], "no-such-offer", jujuparams.OfferReadAccess }, expectedError: "application offer not found", }, { about: "user with consume rights cannot grant any rights", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[2], env.users[4], "test-offer-url", jujuparams.OfferConsumeAccess }, expectedError: "unauthorized", }, { about: "user with read rights cannot grant any rights", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[3], env.users[4], "test-offer-url", jujuparams.OfferConsumeAccess }, expectedError: "unauthorized", }, { about: "admin grants new user admin access - new user has admin access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[4], "test-offer-url", jujuparams.OfferAdminAccess }, expectedAccessLevel: "admin", }, { about: "admin grants new user consume access - new user has consume access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[4], "test-offer-url", jujuparams.OfferConsumeAccess }, expectedAccessLevel: "consume", }, { about: "admin grants new user read access - new user has read access", - parameterFunc: func(env *environment) (dbmodel.User, dbmodel.User, string, jujuparams.OfferAccessPermission) { + parameterFunc: func(env *environment) (dbmodel.Identity, dbmodel.Identity, string, jujuparams.OfferAccessPermission) { return env.users[0], env.users[4], "test-offer-url", jujuparams.OfferReadAccess }, expectedAccessLevel: "read", @@ -568,17 +568,17 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { err = db.Migrate(ctx, false) c.Assert(err, qt.IsNil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) - u1 := dbmodel.User{ + u1 := dbmodel.Identity{ Username: "eve@external", } c.Assert(db.DB.Create(&u1).Error, qt.IsNil) - u2 := dbmodel.User{ + u2 := dbmodel.Identity{ Username: "bob@external", } c.Assert(db.DB.Create(&u2).Error, qt.IsNil) @@ -659,7 +659,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { c.Assert(err, qt.IsNil) everyoneTag := names.NewUserTag(ofganames.EveryoneUser) - uAll := dbmodel.User{ + uAll := dbmodel.Identity{ Username: everyoneTag.Id(), } c.Assert(db.DB.Create(&uAll).Error, qt.IsNil) @@ -723,7 +723,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { tests := []struct { about string - user *dbmodel.User + user *dbmodel.Identity details jujuparams.ConsumeOfferDetails expectedOfferDetails jujuparams.ConsumeOfferDetails expectedError string @@ -942,17 +942,17 @@ func TestGetApplicationOffer(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - u1 := dbmodel.User{ + u1 := dbmodel.Identity{ Username: "eve@external", } c.Assert(j.Database.DB.Create(&u1).Error, qt.IsNil) - u2 := dbmodel.User{ + u2 := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&u2).Error, qt.IsNil) @@ -1053,7 +1053,7 @@ func TestGetApplicationOffer(t *testing.T) { tests := []struct { about string - user *dbmodel.User + user *dbmodel.Identity offerURL string expectedOfferDetails jujuparams.ApplicationOfferAdminDetails expectedError string @@ -1187,7 +1187,7 @@ func TestOffer(t *testing.T) { getApplicationOffer func(context.Context, *jujuparams.ApplicationOfferAdminDetails) error grantApplicationOfferAccess func(context.Context, string, names.UserTag, jujuparams.OfferAccessPermission) error offer func(context.Context, crossmodel.OfferURL, jujuparams.AddApplicationOffer) error - createEnv func(*qt.C, db.Database, *openfga.OFGAClient) (dbmodel.User, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) + createEnv func(*qt.C, db.Database, *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) }{{ about: "all ok", getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetails) error { @@ -1241,10 +1241,10 @@ func TestOffer(t *testing.T) { offer: func(context.Context, crossmodel.OfferURL, jujuparams.AddApplicationOffer) error { return nil }, - createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.User, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { + createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { ctx := context.Background() - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1363,10 +1363,10 @@ func TestOffer(t *testing.T) { offer: func(context.Context, crossmodel.OfferURL, jujuparams.AddApplicationOffer) error { return errors.E("a silly error") }, - createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.User, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { + createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { ctx := context.Background() - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1451,8 +1451,8 @@ func TestOffer(t *testing.T) { offer: func(context.Context, crossmodel.OfferURL, jujuparams.AddApplicationOffer) error { return nil }, - createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.User, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { - u := dbmodel.User{ + createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { + u := dbmodel.Identity{ Username: "alice@external", } @@ -1484,10 +1484,10 @@ func TestOffer(t *testing.T) { offer: func(context.Context, crossmodel.OfferURL, jujuparams.AddApplicationOffer) error { return errors.E(errors.CodeNotFound, "application test-app") }, - createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.User, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { + createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { ctx := context.Background() - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1573,15 +1573,15 @@ func TestOffer(t *testing.T) { offer: func(context.Context, crossmodel.OfferURL, jujuparams.AddApplicationOffer) error { return nil }, - createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.User, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { + createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { ctx := context.Background() - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) - u1 := dbmodel.User{ + u1 := dbmodel.Identity{ Username: "eve@external", } c.Assert(db.DB.Create(&u1).Error, qt.IsNil) @@ -1666,10 +1666,10 @@ func TestOffer(t *testing.T) { offer: func(context.Context, crossmodel.OfferURL, jujuparams.AddApplicationOffer) error { return nil }, - createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.User, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { + createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { ctx := context.Background() - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1754,10 +1754,10 @@ func TestOffer(t *testing.T) { offer: func(context.Context, crossmodel.OfferURL, jujuparams.AddApplicationOffer) error { return errors.E("application offer already exists") }, - createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.User, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { + createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { ctx := context.Background() - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1884,10 +1884,10 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { ctx := context.Background() now := time.Now().UTC().Round(time.Millisecond) - createEnv := func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.User, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { + createEnv := func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { ctx := context.Background() - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -2195,42 +2195,42 @@ func TestDestroyOffer(t *testing.T) { tests := []struct { about string - parameterFunc func(*environment) (dbmodel.User, string) + parameterFunc func(*environment) (dbmodel.Identity, string) destroyError string expectedError string }{{ about: "admin allowed to destroy an offer", - parameterFunc: func(env *environment) (dbmodel.User, string) { + parameterFunc: func(env *environment) (dbmodel.Identity, string) { return env.users[0], "test-offer-url" }, }, { about: "user with consume access not allowed to destroy an offer", - parameterFunc: func(env *environment) (dbmodel.User, string) { + parameterFunc: func(env *environment) (dbmodel.Identity, string) { return env.users[2], "test-offer-url" }, expectedError: "unauthorized", }, { about: "user with read access not allowed to destroy an offer", - parameterFunc: func(env *environment) (dbmodel.User, string) { + parameterFunc: func(env *environment) (dbmodel.Identity, string) { return env.users[3], "test-offer-url" }, expectedError: "unauthorized", }, { about: "user without access not allowed to destroy an offer", - parameterFunc: func(env *environment) (dbmodel.User, string) { + parameterFunc: func(env *environment) (dbmodel.Identity, string) { return env.users[4], "test-offer-url" }, expectedError: "unauthorized", }, { about: "offer not found", - parameterFunc: func(env *environment) (dbmodel.User, string) { + parameterFunc: func(env *environment) (dbmodel.Identity, string) { return env.users[0], "no-such-offer" }, expectedError: "application offer not found", }, { about: "controller returns an error", destroyError: "a silly error", - parameterFunc: func(env *environment) (dbmodel.User, string) { + parameterFunc: func(env *environment) (dbmodel.Identity, string) { return env.users[0], "test-offer-url" }, expectedError: "a silly error", @@ -2484,12 +2484,12 @@ func TestFindApplicationOffers(t *testing.T) { tests := []struct { about string - parameterFunc func(*environment) (dbmodel.User, string, []jujuparams.OfferFilter) + parameterFunc func(*environment) (dbmodel.Identity, string, []jujuparams.OfferFilter) expectedError string expectedOffer *dbmodel.ApplicationOffer }{{ about: "find an offer as an offer consumer", - parameterFunc: func(env *environment) (dbmodel.User, string, []jujuparams.OfferFilter) { + parameterFunc: func(env *environment) (dbmodel.Identity, string, []jujuparams.OfferFilter) { return env.users[2], "consume", []jujuparams.OfferFilter{{ OfferName: "test-offer", }} @@ -2497,7 +2497,7 @@ func TestFindApplicationOffers(t *testing.T) { expectedOffer: &expectedOffer, }, { about: "find an offer as model admin", - parameterFunc: func(env *environment) (dbmodel.User, string, []jujuparams.OfferFilter) { + parameterFunc: func(env *environment) (dbmodel.Identity, string, []jujuparams.OfferFilter) { return env.users[0], "admin", []jujuparams.OfferFilter{{ OfferName: "test-offer", }} @@ -2505,7 +2505,7 @@ func TestFindApplicationOffers(t *testing.T) { expectedOffer: &expectedOffer, }, { about: "find an offer as offer admin", - parameterFunc: func(env *environment) (dbmodel.User, string, []jujuparams.OfferFilter) { + parameterFunc: func(env *environment) (dbmodel.Identity, string, []jujuparams.OfferFilter) { return env.users[5], "admin", []jujuparams.OfferFilter{{ OfferName: "test-offer", }} @@ -2513,7 +2513,7 @@ func TestFindApplicationOffers(t *testing.T) { expectedOffer: &expectedOffer, }, { about: "find an offer as superuser", - parameterFunc: func(env *environment) (dbmodel.User, string, []jujuparams.OfferFilter) { + parameterFunc: func(env *environment) (dbmodel.Identity, string, []jujuparams.OfferFilter) { return env.users[6], "admin", []jujuparams.OfferFilter{{ OfferName: "test-offer", }} @@ -2521,14 +2521,14 @@ func TestFindApplicationOffers(t *testing.T) { expectedOffer: &expectedOffer, }, { about: "offer not found", - parameterFunc: func(env *environment) (dbmodel.User, string, []jujuparams.OfferFilter) { + parameterFunc: func(env *environment) (dbmodel.Identity, string, []jujuparams.OfferFilter) { return env.users[0], "admin", []jujuparams.OfferFilter{{ OfferName: "no-such-offer", }} }, }, { about: "user without access cannot find offers", - parameterFunc: func(env *environment) (dbmodel.User, string, []jujuparams.OfferFilter) { + parameterFunc: func(env *environment) (dbmodel.Identity, string, []jujuparams.OfferFilter) { return env.users[4], "", []jujuparams.OfferFilter{{ OfferName: "test-offer", }} diff --git a/internal/jimm/cloud.go b/internal/jimm/cloud.go index 52abbe0a3..51a7be859 100644 --- a/internal/jimm/cloud.go +++ b/internal/jimm/cloud.go @@ -26,7 +26,7 @@ func (j *JIMM) GetUserCloudAccess(ctx context.Context, user *openfga.User, cloud if accessLevel == ofganames.NoRelation { everyoneTag := names.NewUserTag(ofganames.EveryoneUser) everyone := openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: everyoneTag.Id(), }, j.OpenFGAClient, @@ -99,7 +99,7 @@ func (j *JIMM) ForEachUserCloud(ctx context.Context, user *openfga.User, f func( } // Also include "public" clouds - everyoneDB := dbmodel.User{ + everyoneDB := dbmodel.Identity{ Username: ofganames.EveryoneUser, } everyone := openfga.NewUser(&everyoneDB, j.OpenFGAClient) @@ -528,7 +528,7 @@ func (j *JIMM) GrantCloudAccess(ctx context.Context, user *openfga.User, ct name } err = j.doCloudAdmin(ctx, user, ct, func(_ *dbmodel.Cloud, _ API) error { - targetUser := &dbmodel.User{} + targetUser := &dbmodel.Identity{} targetUser.SetTag(ut) if err := j.Database.GetUser(ctx, targetUser); err != nil { return err @@ -593,7 +593,7 @@ func (j *JIMM) RevokeCloudAccess(ctx context.Context, user *openfga.User, ct nam } err = j.doCloudAdmin(ctx, user, ct, func(_ *dbmodel.Cloud, _ API) error { - targetUser := &dbmodel.User{} + targetUser := &dbmodel.Identity{} targetUser.SetTag(ut) if err := j.Database.GetUser(ctx, targetUser); err != nil { return err diff --git a/internal/jimm/cloud_test.go b/internal/jimm/cloud_test.go index 4ba9f2039..e12c696db 100644 --- a/internal/jimm/cloud_test.go +++ b/internal/jimm/cloud_test.go @@ -41,22 +41,22 @@ func TestGetCloud(t *testing.T) { c.Assert(err, qt.IsNil) alice := openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: "alice@external", }, client, ) bob := openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: "bob@external", }, client, ) - charlie := openfga.NewUser(&dbmodel.User{Username: "charlie@external"}, client) + charlie := openfga.NewUser(&dbmodel.Identity{Username: "charlie@external"}, client) // daphne is a jimm administrator daphne := openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: "daphne@external", }, client, @@ -69,7 +69,7 @@ func TestGetCloud(t *testing.T) { c.Assert(err, qt.IsNil) everyone := openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: ofganames.EveryoneUser, }, client, @@ -169,25 +169,25 @@ func TestForEachCloud(t *testing.T) { c.Assert(err, qt.IsNil) alice := openfga.NewUser( - &dbmodel.User{Username: "alice@external"}, + &dbmodel.Identity{Username: "alice@external"}, client, ) bob := openfga.NewUser( - &dbmodel.User{Username: "bob@external"}, + &dbmodel.Identity{Username: "bob@external"}, client, ) charlie := openfga.NewUser( - &dbmodel.User{Username: "charlie@external"}, + &dbmodel.Identity{Username: "charlie@external"}, client, ) daphne := openfga.NewUser( - &dbmodel.User{Username: "daphne@external"}, + &dbmodel.Identity{Username: "daphne@external"}, client, ) daphne.JimmAdmin = true everyone := openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: ofganames.EveryoneUser, }, client, diff --git a/internal/jimm/cloudcredential.go b/internal/jimm/cloudcredential.go index c9c6edc03..e8cb78091 100644 --- a/internal/jimm/cloudcredential.go +++ b/internal/jimm/cloudcredential.go @@ -48,7 +48,7 @@ func (j *JIMM) GetCloudCredential(ctx context.Context, user *openfga.User, tag n // RevokeCloudCredential checks that the credential with the given path // can be revoked and revokes the credential. -func (j *JIMM) RevokeCloudCredential(ctx context.Context, user *dbmodel.User, tag names.CloudCredentialTag, force bool) error { +func (j *JIMM) RevokeCloudCredential(ctx context.Context, user *dbmodel.Identity, tag names.CloudCredentialTag, force bool) error { const op = errors.Op("jimm.RevokeCloudCredential") if user.Username != tag.Owner().Id() { @@ -140,7 +140,7 @@ func (j *JIMM) UpdateCloudCredential(ctx context.Context, user *openfga.User, ar return result, errors.E(op, errors.CodeUnauthorized, "unauthorized") } // ensure the user we are adding the credential for exists. - var u2 dbmodel.User + var u2 dbmodel.Identity u2.SetTag(args.CredentialTag.Owner()) if err := j.Database.GetUser(ctx, &u2); err != nil { return result, errors.E(op, err) @@ -284,7 +284,7 @@ func (j *JIMM) updateControllerCloudCredential( // calling the function will not contain any attributes, // GetCloudCredentialAttributes should be used to retrive the credential // attributes if needed. The given function should not update the database. -func (j *JIMM) ForEachUserCloudCredential(ctx context.Context, u *dbmodel.User, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error { +func (j *JIMM) ForEachUserCloudCredential(ctx context.Context, u *dbmodel.Identity, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error { const op = errors.Op("jimm.ForEachUserCloudCredential") var cloud string diff --git a/internal/jimm/cloudcredential_test.go b/internal/jimm/cloudcredential_test.go index 4fff4fe9b..038165f38 100644 --- a/internal/jimm/cloudcredential_test.go +++ b/internal/jimm/cloudcredential_test.go @@ -39,12 +39,12 @@ func TestUpdateCloudCredential(t *testing.T) { checkCredentialErrors []error updateCredentialErrors []error jimmAdmin bool - createEnv func(*qt.C, *jimm.JIMM, *openfga.OFGAClient) (*dbmodel.User, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) + createEnv func(*qt.C, *jimm.JIMM, *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) }{{ about: "all ok", jimmAdmin: true, - createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.User, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { - u := dbmodel.User{ + createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -144,7 +144,7 @@ func TestUpdateCloudCredential(t *testing.T) { c.Assert(err, qt.IsNil) // Clear some fields we don't need. // TODO(mhilton) don't fetch these in the first place. - m.Owner = dbmodel.User{} + m.Owner = dbmodel.Identity{} m.Controller = dbmodel.Controller{} m.CloudCredential = dbmodel.CloudCredential{} m.CloudRegion = dbmodel.CloudRegion{} @@ -157,8 +157,8 @@ func TestUpdateCloudCredential(t *testing.T) { about: "update credential error returned by controller", jimmAdmin: true, updateCredentialErrors: []error{nil, errors.E("test error")}, - createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.User, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { - u := dbmodel.User{ + createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -248,8 +248,8 @@ func TestUpdateCloudCredential(t *testing.T) { jimmAdmin: true, checkCredentialErrors: []error{errors.E("test error")}, updateCredentialErrors: []error{nil}, - createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.User, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { - u := dbmodel.User{ + createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -332,8 +332,8 @@ func TestUpdateCloudCredential(t *testing.T) { }, { about: "user is controller superuser", jimmAdmin: true, - createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.User, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { - u := dbmodel.User{ + createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -344,7 +344,7 @@ func TestUpdateCloudCredential(t *testing.T) { err := alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) - eve := dbmodel.User{ + eve := dbmodel.Identity{ Username: "eve@external", } c.Assert(j.Database.DB.Create(&eve).Error, qt.IsNil) @@ -431,7 +431,7 @@ func TestUpdateCloudCredential(t *testing.T) { c.Assert(err, qt.IsNil) // Clear some fields we don't need. // TODO(mhilton) don't fetch these in the first place. - m.Owner = dbmodel.User{} + m.Owner = dbmodel.Identity{} m.Controller = dbmodel.Controller{} m.CloudCredential = dbmodel.CloudCredential{} m.CloudRegion = dbmodel.CloudRegion{} @@ -456,8 +456,8 @@ func TestUpdateCloudCredential(t *testing.T) { about: "skip check, which would return an error", checkCredentialErrors: []error{errors.E("test error")}, jimmAdmin: true, - createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.User, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { - u := dbmodel.User{ + createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -558,7 +558,7 @@ func TestUpdateCloudCredential(t *testing.T) { c.Assert(err, qt.IsNil) // Clear some fields we don't need. // TODO(mhilton) don't fetch these in the first place. - m.Owner = dbmodel.User{} + m.Owner = dbmodel.Identity{} m.Controller = dbmodel.Controller{} m.CloudCredential = dbmodel.CloudCredential{} m.CloudRegion = dbmodel.CloudRegion{} @@ -569,8 +569,8 @@ func TestUpdateCloudCredential(t *testing.T) { }, { about: "skip update", jimmAdmin: true, - createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.User, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { - u := dbmodel.User{ + createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -664,7 +664,7 @@ func TestUpdateCloudCredential(t *testing.T) { c.Assert(err, qt.IsNil) // Clear some fields we don't need. // TODO(mhilton) don't fetch these in the first place. - m.Owner = dbmodel.User{} + m.Owner = dbmodel.Identity{} m.Controller = dbmodel.Controller{} m.CloudCredential = dbmodel.CloudCredential{} m.CloudRegion = dbmodel.CloudRegion{} @@ -872,11 +872,11 @@ func TestRevokeCloudCredential(t *testing.T) { tests := []struct { about string revokeCredentialErrors []error - createEnv func(*qt.C, *jimm.JIMM, *openfga.OFGAClient) (*dbmodel.User, names.CloudCredentialTag, string) + createEnv func(*qt.C, *jimm.JIMM, *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) }{{ about: "credential revoked", - createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.User, names.CloudCredentialTag, string) { - u := dbmodel.User{ + createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -949,8 +949,8 @@ func TestRevokeCloudCredential(t *testing.T) { Message: "credential not found", Code: jujuparams.CodeNotFound, }}, - createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.User, names.CloudCredentialTag, string) { - u := dbmodel.User{ + createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1019,8 +1019,8 @@ func TestRevokeCloudCredential(t *testing.T) { }, }, { about: "credential still used by a model", - createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.User, names.CloudCredentialTag, string) { - u := dbmodel.User{ + createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1094,8 +1094,8 @@ func TestRevokeCloudCredential(t *testing.T) { }, }, { about: "user not owner of credentials - unauthorizer error", - createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.User, names.CloudCredentialTag, string) { - u := dbmodel.User{ + createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1112,8 +1112,8 @@ func TestRevokeCloudCredential(t *testing.T) { }, { about: "error revoking credential on controller", revokeCredentialErrors: []error{errors.E("test error")}, - createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.User, names.CloudCredentialTag, string) { - u := dbmodel.User{ + createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1281,11 +1281,11 @@ func TestGetCloudCredential(t *testing.T) { tests := []struct { about string revokeCredentialErrors []error - createEnv func(*qt.C, *jimm.JIMM, *openfga.OFGAClient) (*dbmodel.User, names.CloudCredentialTag, dbmodel.CloudCredential, string) + createEnv func(*qt.C, *jimm.JIMM, *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, dbmodel.CloudCredential, string) }{{ about: "all ok", - createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.User, names.CloudCredentialTag, dbmodel.CloudCredential, string) { - u := dbmodel.User{ + createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, dbmodel.CloudCredential, string) { + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1354,8 +1354,8 @@ func TestGetCloudCredential(t *testing.T) { }, }, { about: "credential not found", - createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.User, names.CloudCredentialTag, dbmodel.CloudCredential, string) { - u := dbmodel.User{ + createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, dbmodel.CloudCredential, string) { + u := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) diff --git a/internal/jimm/clouddefaults.go b/internal/jimm/clouddefaults.go index 59420a15b..64a06234c 100644 --- a/internal/jimm/clouddefaults.go +++ b/internal/jimm/clouddefaults.go @@ -16,7 +16,7 @@ const ( ) // SetModelDefaults writes new default model setting values for the specified cloud/region. -func (j *JIMM) SetModelDefaults(ctx context.Context, user *dbmodel.User, cloudTag names.CloudTag, region string, configs map[string]interface{}) error { +func (j *JIMM) SetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, configs map[string]interface{}) error { const op = errors.Op("jimm.SetModelDefaults") var keys strings.Builder @@ -66,7 +66,7 @@ func (j *JIMM) SetModelDefaults(ctx context.Context, user *dbmodel.User, cloudTa } // UnsetModelDefaults resets default model setting values for the specified cloud/region. -func (j *JIMM) UnsetModelDefaults(ctx context.Context, user *dbmodel.User, cloudTag names.CloudTag, region string, keys []string) error { +func (j *JIMM) UnsetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error { const op = errors.Op("jimm.UnsetModelDefaults") defaults := dbmodel.CloudDefaults{ @@ -84,7 +84,7 @@ func (j *JIMM) UnsetModelDefaults(ctx context.Context, user *dbmodel.User, cloud } // ModelDefaultsForCloud returns the default config values for the specified cloud. -func (j *JIMM) ModelDefaultsForCloud(ctx context.Context, user *dbmodel.User, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) { +func (j *JIMM) ModelDefaultsForCloud(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) { const op = errors.Op("jimm.ModelDefaultsForCloud") result := jujuparams.ModelDefaultsResult{ Config: make(map[string]jujuparams.ModelDefaults), diff --git a/internal/jimm/clouddefaults_test.go b/internal/jimm/clouddefaults_test.go index dc97f33f8..6a43c0df9 100644 --- a/internal/jimm/clouddefaults_test.go +++ b/internal/jimm/clouddefaults_test.go @@ -26,7 +26,7 @@ func TestSetCloudDefaults(t *testing.T) { now := time.Now() type testConfig struct { - user *dbmodel.User + user *dbmodel.Identity cloud names.CloudTag region string defaults map[string]interface{} @@ -41,7 +41,7 @@ func TestSetCloudDefaults(t *testing.T) { }{{ about: "defaults do not exist yet - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -81,7 +81,7 @@ func TestSetCloudDefaults(t *testing.T) { }, { about: "set defaults without region - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -120,7 +120,7 @@ func TestSetCloudDefaults(t *testing.T) { }, { about: "defaults already exist - defaults updated", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -173,7 +173,7 @@ func TestSetCloudDefaults(t *testing.T) { }, { about: "cloudregion does not exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -203,7 +203,7 @@ func TestSetCloudDefaults(t *testing.T) { }, { about: "cannot set agent-version", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -271,7 +271,7 @@ func TestUnsetCloudDefaults(t *testing.T) { now := time.Now() type testConfig struct { - user *dbmodel.User + user *dbmodel.Identity cloud names.CloudTag region string keys []string @@ -286,7 +286,7 @@ func TestUnsetCloudDefaults(t *testing.T) { }{{ about: "all ok - keys removed from the defaults map", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -341,7 +341,7 @@ func TestUnsetCloudDefaults(t *testing.T) { }, { about: "unset without region - keys removed from the defaults map", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -395,7 +395,7 @@ func TestUnsetCloudDefaults(t *testing.T) { }, { about: "cloudregiondefaults not found", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -471,12 +471,12 @@ func TestModelDefaultsForCloud(t *testing.T) { err := j.Database.Migrate(ctx, true) c.Assert(err, qt.Equals, nil) - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) - user1 := dbmodel.User{ + user1 := dbmodel.Identity{ Username: "alice@external", } c.Assert(j.Database.DB.Create(&user1).Error, qt.IsNil) diff --git a/internal/jimm/controller.go b/internal/jimm/controller.go index 505c21165..d5cc10654 100644 --- a/internal/jimm/controller.go +++ b/internal/jimm/controller.go @@ -169,7 +169,7 @@ func (j *JIMM) AddController(ctx context.Context, user *openfga.User, ctl *dbmod if cloud.ResourceTag().String() == ms.CloudTag { everyoneTag := names.NewUserTag(ofganames.EveryoneUser) everyone := openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: everyoneTag.Id(), }, j.OpenFGAClient, @@ -300,7 +300,7 @@ func (j *JIMM) GetJimmControllerAccess(ctx context.Context, user *openfga.User, return "", errors.E(op, errors.CodeUnauthorized, "unauthorized") } - var targetUser dbmodel.User + var targetUser dbmodel.Identity targetUser.SetTag(tag) targetUserTag := openfga.NewUser(&targetUser, j.OpenFGAClient) @@ -378,7 +378,7 @@ func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerNa if ownerTag.IsLocal() { return errors.E(op, "cannot import model from local user, try --owner to switch the model owner") } - ownerUser := dbmodel.User{} + ownerUser := dbmodel.Identity{} ownerUser.SetTag(ownerTag) err = j.Database.GetUser(ctx, &ownerUser) if err != nil { @@ -527,7 +527,7 @@ func (j *JIMM) SetControllerConfig(ctx context.Context, user *openfga.User, args } // GetControllerConfig returns jimm's controller config. -func (j *JIMM) GetControllerConfig(ctx context.Context, u *dbmodel.User) (*dbmodel.ControllerConfig, error) { +func (j *JIMM) GetControllerConfig(ctx context.Context, u *dbmodel.Identity) (*dbmodel.ControllerConfig, error) { const op = errors.Op("jimm.GetControllerConfig") config := dbmodel.ControllerConfig{ Name: "jimm", diff --git a/internal/jimm/controller_test.go b/internal/jimm/controller_test.go index ad01ce806..af1f1bdc8 100644 --- a/internal/jimm/controller_test.go +++ b/internal/jimm/controller_test.go @@ -147,7 +147,7 @@ func TestAddController(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } @@ -315,7 +315,7 @@ func TestAddControllerWithVault(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } alice := openfga.NewUser(&u, ofgaClient) @@ -602,7 +602,7 @@ func TestImportModel(t *testing.T) { String: "00000002-0000-0000-0000-000000000001", Valid: true, }, - Owner: dbmodel.User{ + Owner: dbmodel.Identity{ Username: "alice@external", DisplayName: "Alice", }, @@ -690,7 +690,7 @@ func TestImportModel(t *testing.T) { String: "00000002-0000-0000-0000-000000000001", Valid: true, }, - Owner: dbmodel.User{ + Owner: dbmodel.Identity{ Username: "alice@external", DisplayName: "Alice", }, @@ -1121,7 +1121,7 @@ func TestGetControllerConfig(t *testing.T) { }) c.Assert(err, qt.Equals, nil) - cfg, err := j.GetControllerConfig(ctx, user.User) + cfg, err := j.GetControllerConfig(ctx, user.Identity) if test.expectedError == "" { c.Assert(err, qt.IsNil) c.Assert(cfg, jimmtest.DBObjectEquals, &test.expectedConfig) @@ -1436,7 +1436,7 @@ func TestInitiateMigration(t *testing.T) { about: "model migration initiated successfully", user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: "alice@external", }, client, @@ -1461,7 +1461,7 @@ func TestInitiateMigration(t *testing.T) { about: "model not found", user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: "alice@external", }, client, @@ -1481,7 +1481,7 @@ func TestInitiateMigration(t *testing.T) { about: "InitiateMigration call fails", user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: "alice@external", }, client, @@ -1502,7 +1502,7 @@ func TestInitiateMigration(t *testing.T) { about: "non-admin-user gets unauthorized error", user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: "bob@external", }, client, @@ -1521,7 +1521,7 @@ func TestInitiateMigration(t *testing.T) { about: "invalid model tag", user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: "alice@external", }, client, @@ -1540,7 +1540,7 @@ func TestInitiateMigration(t *testing.T) { about: "invalid target controller tag", user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: "alice@external", }, client, @@ -1559,7 +1559,7 @@ func TestInitiateMigration(t *testing.T) { about: "invalid target user tag", user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: "alice@external", }, client, @@ -1578,7 +1578,7 @@ func TestInitiateMigration(t *testing.T) { about: "invalid macaroon data", user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: "alice@external", }, client, diff --git a/internal/jimm/export_test.go b/internal/jimm/export_test.go index b302efcd7..4c52cde7a 100644 --- a/internal/jimm/export_test.go +++ b/internal/jimm/export_test.go @@ -43,6 +43,6 @@ func NewWatcherWithDeltaProcessedChannel(db db.Database, dialer Dialer, pubsub P } } -func (j *JIMM) ListApplicationOfferUsers(ctx context.Context, offer names.ApplicationOfferTag, user *dbmodel.User, accessLevel string) ([]jujuparams.OfferUserDetails, error) { +func (j *JIMM) ListApplicationOfferUsers(ctx context.Context, offer names.ApplicationOfferTag, user *dbmodel.Identity, accessLevel string) ([]jujuparams.OfferUserDetails, error) { return j.listApplicationOfferUsers(ctx, offer, user, accessLevel) } diff --git a/internal/jimm/jimm_test.go b/internal/jimm/jimm_test.go index f55d330f0..bfcc4ccc0 100644 --- a/internal/jimm/jimm_test.go +++ b/internal/jimm/jimm_test.go @@ -45,32 +45,32 @@ func TestFindAuditEvents(t *testing.T) { err = j.Database.Migrate(ctx, true) c.Assert(err, qt.Equals, nil) - admin := openfga.NewUser(&dbmodel.User{Username: "alice@external"}, client) + admin := openfga.NewUser(&dbmodel.Identity{Username: "alice@external"}, client) err = admin.SetControllerAccess(ctx, j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) - privileged := openfga.NewUser(&dbmodel.User{Username: "bob@external"}, client) + privileged := openfga.NewUser(&dbmodel.Identity{Username: "bob@external"}, client) err = privileged.SetControllerAccess(ctx, j.ResourceTag(), ofganames.AuditLogViewerRelation) c.Assert(err, qt.IsNil) - unprivileged := openfga.NewUser(&dbmodel.User{Username: "eve@external"}, client) + unprivileged := openfga.NewUser(&dbmodel.Identity{Username: "eve@external"}, client) events := []dbmodel.AuditLogEntry{{ Time: now, - UserTag: admin.User.Tag().String(), + UserTag: admin.Identity.Tag().String(), FacadeMethod: "Login", }, { Time: now.Add(time.Hour), - UserTag: admin.User.Tag().String(), + UserTag: admin.Identity.Tag().String(), FacadeMethod: "AddModel", }, { Time: now.Add(2 * time.Hour), - UserTag: privileged.User.Tag().String(), + UserTag: privileged.Identity.Tag().String(), Model: "TestModel", FacadeMethod: "Deploy", }, { Time: now.Add(3 * time.Hour), - UserTag: privileged.User.Tag().String(), + UserTag: privileged.Identity.Tag().String(), Model: "TestModel", FacadeMethod: "DestroyModel", }} @@ -220,7 +220,7 @@ func TestListControllers(t *testing.T) { tests := []struct { about string - user dbmodel.User + user dbmodel.Identity jimmAdmin bool expectedControllers []dbmodel.Controller expectedError string @@ -308,7 +308,7 @@ func TestSetControllerDeprecated(t *testing.T) { tests := []struct { about string - user dbmodel.User + user dbmodel.Identity jimmAdmin bool deprecated bool expectedError string diff --git a/internal/jimm/model.go b/internal/jimm/model.go index 7e9b07d5d..af2debc7e 100644 --- a/internal/jimm/model.go +++ b/internal/jimm/model.go @@ -102,7 +102,7 @@ type modelBuilder struct { name string config map[string]interface{} - owner *dbmodel.User + owner *dbmodel.Identity credential *dbmodel.CloudCredential controller *dbmodel.Controller cloud *dbmodel.Cloud @@ -146,7 +146,7 @@ func (b *modelBuilder) jujuModelCreateArgs() (*jujuparams.ModelCreateArgs, error } // WithOwner returns a builder with the specified owner. -func (b *modelBuilder) WithOwner(owner *dbmodel.User) *modelBuilder { +func (b *modelBuilder) WithOwner(owner *dbmodel.Identity) *modelBuilder { if b.err != nil { return b } @@ -519,7 +519,7 @@ func (b *modelBuilder) JujuModelInfo() *jujuparams.ModelInfo { func (j *JIMM) AddModel(ctx context.Context, user *openfga.User, args *ModelCreateArgs) (_ *jujuparams.ModelInfo, err error) { const op = errors.Op("jimm.AddModel") - owner := &dbmodel.User{ + owner := &dbmodel.Identity{ Username: args.Owner.Id(), } err = j.Database.GetUser(ctx, owner) @@ -540,7 +540,7 @@ func (j *JIMM) AddModel(ctx context.Context, user *openfga.User, args *ModelCrea } // fetch user model defaults - userConfig, err := j.UserModelDefaults(ctx, user.User) + userConfig, err := j.UserModelDefaults(ctx, user.Identity) if err != nil && errors.ErrorCode(err) != errors.CodeNotFound { return nil, errors.E(op, "failed to fetch cloud defaults") } @@ -850,7 +850,7 @@ func (j *JIMM) GrantModelAccess(ctx context.Context, user *openfga.User, mt name } err = j.doModelAdmin(ctx, user, mt, func(_ *dbmodel.Model, _ API) error { - targetUser := &dbmodel.User{} + targetUser := &dbmodel.Identity{} targetUser.SetTag(ut) if err := j.Database.GetUser(ctx, targetUser); err != nil { return err @@ -928,7 +928,7 @@ func (j *JIMM) RevokeModelAccess(ctx context.Context, user *openfga.User, mt nam } err = j.doModel(ctx, user, mt, requiredAccess, func(_ *dbmodel.Model, _ API) error { - targetUser := &dbmodel.User{} + targetUser := &dbmodel.Identity{} targetUser.SetTag(ut) if err := j.Database.GetUser(ctx, targetUser); err != nil { return err diff --git a/internal/jimm/model_test.go b/internal/jimm/model_test.go index 5f479878c..fedc494e3 100644 --- a/internal/jimm/model_test.go +++ b/internal/jimm/model_test.go @@ -237,7 +237,7 @@ users: String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - Owner: dbmodel.User{ + Owner: dbmodel.Identity{ Username: "alice@external", }, Controller: dbmodel.Controller{ @@ -351,7 +351,7 @@ users: String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - Owner: dbmodel.User{ + Owner: dbmodel.Identity{ Username: "alice@external", }, Controller: dbmodel.Controller{ @@ -444,7 +444,7 @@ users: String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - Owner: dbmodel.User{ + Owner: dbmodel.Identity{ Username: "alice@external", }, Controller: dbmodel.Controller{ @@ -544,7 +544,7 @@ users: String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - Owner: dbmodel.User{ + Owner: dbmodel.Identity{ Username: "bob@external", }, Controller: dbmodel.Controller{ @@ -945,7 +945,7 @@ func TestAddModel(t *testing.T) { isModelAdmin, err := openfga.IsAdministrator( context.Background(), openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: ownerId, }, client, @@ -1296,7 +1296,7 @@ func TestModelInfo(t *testing.T) { env := jimmtest.ParseEnvironment(c, test.env) env.PopulateDBAndPermissions(c, j.ResourceTag(), j.Database, client) - dbUser := &dbmodel.User{ + dbUser := &dbmodel.Identity{ Username: test.username, } user := openfga.NewUser(dbUser, client) @@ -3353,7 +3353,7 @@ var updateModelCredentialTests = []struct { String: "00000002-0000-0000-0000-000000000001", Valid: true, }, - Owner: dbmodel.User{ + Owner: dbmodel.Identity{ Username: "alice@external", }, Controller: dbmodel.Controller{ diff --git a/internal/jimm/user.go b/internal/jimm/user.go index e88061a27..5fd25287d 100644 --- a/internal/jimm/user.go +++ b/internal/jimm/user.go @@ -32,7 +32,7 @@ func (j *JIMM) Authenticate(ctx context.Context, req *jujuparams.LoginRequest) ( } err = j.Database.Transaction(func(tx *db.Database) error { - pu := dbmodel.User{ + pu := dbmodel.Identity{ Username: u.Username, } if err := tx.GetUser(ctx, &pu); err != nil { diff --git a/internal/jimm/user_test.go b/internal/jimm/user_test.go index a085b3bf7..e10d770c5 100644 --- a/internal/jimm/user_test.go +++ b/internal/jimm/user_test.go @@ -52,7 +52,7 @@ func TestAuthenticate(t *testing.T) { c.Assert(err, qt.IsNil) auth.User = openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: "bob@external", DisplayName: "Bob", }, @@ -75,13 +75,13 @@ func TestAuthenticate(t *testing.T) { c.Check(u.Username, qt.Equals, "bob@external") c.Check(u.JimmAdmin, qt.IsTrue) - u2 := dbmodel.User{ + u2 := dbmodel.Identity{ Username: "bob@external", } err = j.Database.GetUser(ctx, &u2) c.Assert(err, qt.IsNil) - c.Check(u2, qt.DeepEquals, dbmodel.User{ + c.Check(u2, qt.DeepEquals, dbmodel.Identity{ Model: u.Model, Username: "bob@external", DisplayName: "Bob", diff --git a/internal/jimm/usermodeldefaults.go b/internal/jimm/usermodeldefaults.go index acd334307..0cbceef04 100644 --- a/internal/jimm/usermodeldefaults.go +++ b/internal/jimm/usermodeldefaults.go @@ -10,7 +10,7 @@ import ( ) // SetUserModelDefaults writes new default model setting values for the user. -func (j *JIMM) SetUserModelDefaults(ctx context.Context, user *dbmodel.User, configs map[string]interface{}) error { +func (j *JIMM) SetUserModelDefaults(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error { const op = errors.Op("jimm.SetUserModelDefaults") for k := range configs { @@ -30,7 +30,7 @@ func (j *JIMM) SetUserModelDefaults(ctx context.Context, user *dbmodel.User, con } // UserModelDefaults returns the default config values for the user. -func (j *JIMM) UserModelDefaults(ctx context.Context, user *dbmodel.User) (map[string]interface{}, error) { +func (j *JIMM) UserModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) { const op = errors.Op("jimm.UserModelDefaults") defaults := dbmodel.UserModelDefaults{ diff --git a/internal/jimm/usermodeldefaults_test.go b/internal/jimm/usermodeldefaults_test.go index 24b22a18d..3caafd51a 100644 --- a/internal/jimm/usermodeldefaults_test.go +++ b/internal/jimm/usermodeldefaults_test.go @@ -24,7 +24,7 @@ func TestSetUserModelDefaults(t *testing.T) { now := time.Now() type testConfig struct { - user *dbmodel.User + user *dbmodel.Identity defaults map[string]interface{} expectedError string expectedDefaults *dbmodel.UserModelDefaults @@ -37,7 +37,7 @@ func TestSetUserModelDefaults(t *testing.T) { }{{ about: "defaults do not exist yet - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -62,7 +62,7 @@ func TestSetUserModelDefaults(t *testing.T) { }, { about: "defaults already exist - defaults updated", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -97,7 +97,7 @@ func TestSetUserModelDefaults(t *testing.T) { }, { about: "cannot set agent-version", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -151,7 +151,7 @@ func TestUserModelDefaults(t *testing.T) { now := time.Now() type testConfig struct { - user *dbmodel.User + user *dbmodel.Identity expectedError string expectedDefaults map[string]interface{} } @@ -163,7 +163,7 @@ func TestUserModelDefaults(t *testing.T) { }{{ about: "defaults do not exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -176,7 +176,7 @@ func TestUserModelDefaults(t *testing.T) { }, { about: "defaults exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) diff --git a/internal/jimm/watcher_test.go b/internal/jimm/watcher_test.go index aeef8e1ce..f917dbcd4 100644 --- a/internal/jimm/watcher_test.go +++ b/internal/jimm/watcher_test.go @@ -386,7 +386,7 @@ var watcherTests = []struct { c.Assert(err, qt.IsNil) // zero any uninteresting associations // TODO(mhilton) don't fetch these in the first place. - model.Owner = dbmodel.User{} + model.Owner = dbmodel.Identity{} model.CloudCredential = dbmodel.CloudCredential{} model.CloudRegion = dbmodel.CloudRegion{} model.Controller = dbmodel.Controller{} @@ -482,7 +482,7 @@ var watcherTests = []struct { c.Assert(err, qt.IsNil) // zero any uninteresting associations // TODO(mhilton) don't fetch these in the first place. - model.Owner = dbmodel.User{} + model.Owner = dbmodel.Identity{} model.CloudCredential = dbmodel.CloudCredential{} model.CloudRegion = dbmodel.CloudRegion{} model.Controller = dbmodel.Controller{} diff --git a/internal/jimmtest/cmp.go b/internal/jimmtest/cmp.go index 8ef28ee08..5413f7d63 100644 --- a/internal/jimmtest/cmp.go +++ b/internal/jimmtest/cmp.go @@ -32,7 +32,7 @@ func ControllerTagCompare(a, b dbmodel.Controller) bool { // determines if two database user objects are equal based on comparing // the Tag values. This is often sufficient where the objects are embedded // in another database object. -func UserTagCompare(a, b dbmodel.User) bool { +func UserTagCompare(a, b dbmodel.Identity) bool { return a.Tag() == b.Tag() } diff --git a/internal/jimmtest/env.go b/internal/jimmtest/env.go index d1bfcdf5a..3d98ac960 100644 --- a/internal/jimmtest/env.go +++ b/internal/jimmtest/env.go @@ -181,7 +181,7 @@ func (m Model) addModelRelations(c *qt.C, jimmTag names.ControllerTag, db db.Dat // addControllerRelations adds permissions the model should have and adds permissions for users to the controller. func (ctl Controller) addControllerRelations(c *qt.C, jimmTag names.ControllerTag, db db.Database, client *openfga.OFGAClient) { if ctl.dbo.AdminUser != "" { - user := openfga.NewUser(&dbmodel.User{ + user := openfga.NewUser(&dbmodel.Identity{ Username: ctl.dbo.AdminUser, }, client) err := user.SetControllerAccess(context.Background(), ctl.dbo.ResourceTag(), ofganames.AdministratorRelation) @@ -484,10 +484,10 @@ type User struct { ControllerAccess string `json:"controller-access"` env *Environment - dbo dbmodel.User + dbo dbmodel.Identity } -func (u *User) DBObject(c *qt.C, db db.Database) dbmodel.User { +func (u *User) DBObject(c *qt.C, db db.Database) dbmodel.Identity { if u.dbo.ID != 0 { return u.dbo } diff --git a/internal/jimmtest/jimm_mock.go b/internal/jimmtest/jimm_mock.go index db9b53885..3397ed99d 100644 --- a/internal/jimmtest/jimm_mock.go +++ b/internal/jimmtest/jimm_mock.go @@ -49,7 +49,7 @@ type JIMM struct { ForEachCloud_ func(ctx context.Context, user *openfga.User, f func(*dbmodel.Cloud) error) error ForEachModel_ func(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error ForEachUserCloud_ func(ctx context.Context, user *openfga.User, f func(*dbmodel.Cloud) error) error - ForEachUserCloudCredential_ func(ctx context.Context, u *dbmodel.User, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error + ForEachUserCloudCredential_ func(ctx context.Context, u *dbmodel.Identity, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error ForEachUserModel_ func(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error FullModelStatus_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, patterns []string) (*jujuparams.FullStatus, error) GetApplicationOffer_ func(ctx context.Context, user *openfga.User, offerURL string) (*jujuparams.ApplicationOfferAdminDetails, error) @@ -57,7 +57,7 @@ type JIMM struct { GetCloud_ func(ctx context.Context, u *openfga.User, tag names.CloudTag) (dbmodel.Cloud, error) GetCloudCredential_ func(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) GetCloudCredentialAttributes_ func(ctx context.Context, u *openfga.User, cred *dbmodel.CloudCredential, hidden bool) (attrs map[string]string, redacted []string, err error) - GetControllerConfig_ func(ctx context.Context, u *dbmodel.User) (*dbmodel.ControllerConfig, error) + GetControllerConfig_ func(ctx context.Context, u *dbmodel.Identity) (*dbmodel.ControllerConfig, error) GetJimmControllerAccess_ func(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error) GetUserCloudAccess_ func(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error) GetUserControllerAccess_ func(ctx context.Context, user *openfga.User, controller names.ControllerTag) (string, error) @@ -70,7 +70,7 @@ type JIMM struct { InitiateMigration_ func(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec, targetControllerID uint) (jujuparams.InitiateMigrationResult, error) ListApplicationOffers_ func(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) ListControllers_ func(ctx context.Context, user *openfga.User) ([]dbmodel.Controller, error) - ModelDefaultsForCloud_ func(ctx context.Context, user *dbmodel.User, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) + ModelDefaultsForCloud_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) ModelInfo_ func(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelInfo, error) ModelStatus_ func(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelStatus, error) Offer_ func(ctx context.Context, user *openfga.User, offer jimm.AddApplicationOfferParams) error @@ -83,19 +83,19 @@ type JIMM struct { ResourceTag_ func() names.ControllerTag RevokeAuditLogAccess_ func(ctx context.Context, user *openfga.User, targetUserTag names.UserTag) error RevokeCloudAccess_ func(ctx context.Context, user *openfga.User, ct names.CloudTag, ut names.UserTag, access string) error - RevokeCloudCredential_ func(ctx context.Context, user *dbmodel.User, tag names.CloudCredentialTag, force bool) error + RevokeCloudCredential_ func(ctx context.Context, user *dbmodel.Identity, tag names.CloudCredentialTag, force bool) error RevokeModelAccess_ func(ctx context.Context, user *openfga.User, mt names.ModelTag, ut names.UserTag, access jujuparams.UserAccessPermission) error RevokeOfferAccess_ func(ctx context.Context, user *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) (err error) SetControllerConfig_ func(ctx context.Context, u *openfga.User, args jujuparams.ControllerConfigSet) error SetControllerDeprecated_ func(ctx context.Context, user *openfga.User, controllerName string, deprecated bool) error - SetModelDefaults_ func(ctx context.Context, user *dbmodel.User, cloudTag names.CloudTag, region string, configs map[string]interface{}) error - SetUserModelDefaults_ func(ctx context.Context, user *dbmodel.User, configs map[string]interface{}) error - UnsetModelDefaults_ func(ctx context.Context, user *dbmodel.User, cloudTag names.CloudTag, region string, keys []string) error + SetModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, configs map[string]interface{}) error + SetUserModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error + UnsetModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error UpdateApplicationOffer_ func(ctx context.Context, controller *dbmodel.Controller, offerUUID string, removed bool) error UpdateCloud_ func(ctx context.Context, u *openfga.User, ct names.CloudTag, cloud jujuparams.Cloud) error UpdateCloudCredential_ func(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) UpdateMigratedModel_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetControllerName string) error - UserModelDefaults_ func(ctx context.Context, user *dbmodel.User) (map[string]interface{}, error) + UserModelDefaults_ func(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) ValidateModelUpgrade_ func(ctx context.Context, u *openfga.User, mt names.ModelTag, force bool) error WatchAllModelSummaries_ func(ctx context.Context, controller *dbmodel.Controller) (_ func() error, err error) } @@ -220,7 +220,7 @@ func (j *JIMM) ForEachUserCloud(ctx context.Context, user *openfga.User, f func( } return j.ForEachUserCloud_(ctx, user, f) } -func (j *JIMM) ForEachUserCloudCredential(ctx context.Context, u *dbmodel.User, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error { +func (j *JIMM) ForEachUserCloudCredential(ctx context.Context, u *dbmodel.Identity, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error { if j.ForEachUserCloudCredential_ == nil { return errors.E(errors.CodeNotImplemented) } @@ -268,7 +268,7 @@ func (j *JIMM) GetCloudCredentialAttributes(ctx context.Context, u *openfga.User } return j.GetCloudCredentialAttributes_(ctx, u, cred, hidden) } -func (j *JIMM) GetControllerConfig(ctx context.Context, u *dbmodel.User) (*dbmodel.ControllerConfig, error) { +func (j *JIMM) GetControllerConfig(ctx context.Context, u *dbmodel.Identity) (*dbmodel.ControllerConfig, error) { if j.GetControllerConfig_ == nil { return nil, errors.E(errors.CodeNotImplemented) } @@ -346,7 +346,7 @@ func (j *JIMM) ListControllers(ctx context.Context, user *openfga.User) ([]dbmod } return j.ListControllers_(ctx, user) } -func (j *JIMM) ModelDefaultsForCloud(ctx context.Context, user *dbmodel.User, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) { +func (j *JIMM) ModelDefaultsForCloud(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) { if j.ModelDefaultsForCloud_ == nil { return jujuparams.ModelDefaultsResult{}, errors.E(errors.CodeNotImplemented) } @@ -424,7 +424,7 @@ func (j *JIMM) RevokeCloudAccess(ctx context.Context, user *openfga.User, ct nam } return j.RevokeCloudAccess_(ctx, user, ct, ut, access) } -func (j *JIMM) RevokeCloudCredential(ctx context.Context, user *dbmodel.User, tag names.CloudCredentialTag, force bool) error { +func (j *JIMM) RevokeCloudCredential(ctx context.Context, user *dbmodel.Identity, tag names.CloudCredentialTag, force bool) error { if j.RevokeCloudCredential_ == nil { return errors.E(errors.CodeNotImplemented) } @@ -454,19 +454,19 @@ func (j *JIMM) SetControllerDeprecated(ctx context.Context, user *openfga.User, } return j.SetControllerDeprecated_(ctx, user, controllerName, deprecated) } -func (j *JIMM) SetModelDefaults(ctx context.Context, user *dbmodel.User, cloudTag names.CloudTag, region string, configs map[string]interface{}) error { +func (j *JIMM) SetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, configs map[string]interface{}) error { if j.SetModelDefaults_ == nil { return errors.E(errors.CodeNotImplemented) } return j.SetModelDefaults_(ctx, user, cloudTag, region, configs) } -func (j *JIMM) SetUserModelDefaults(ctx context.Context, user *dbmodel.User, configs map[string]interface{}) error { +func (j *JIMM) SetUserModelDefaults(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error { if j.SetUserModelDefaults_ == nil { return errors.E(errors.CodeNotImplemented) } return j.SetUserModelDefaults_(ctx, user, configs) } -func (j *JIMM) UnsetModelDefaults(ctx context.Context, user *dbmodel.User, cloudTag names.CloudTag, region string, keys []string) error { +func (j *JIMM) UnsetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error { if j.UnsetModelDefaults_ == nil { return errors.E(errors.CodeNotImplemented) } @@ -496,7 +496,7 @@ func (j *JIMM) UpdateMigratedModel(ctx context.Context, user *openfga.User, mode } return j.UpdateMigratedModel_(ctx, user, modelTag, targetControllerName) } -func (j *JIMM) UserModelDefaults(ctx context.Context, user *dbmodel.User) (map[string]interface{}, error) { +func (j *JIMM) UserModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) { if j.UserModelDefaults_ == nil { return nil, errors.E(errors.CodeNotImplemented) } diff --git a/internal/jimmtest/suite.go b/internal/jimmtest/suite.go index 76c909d78..694315d11 100644 --- a/internal/jimmtest/suite.go +++ b/internal/jimmtest/suite.go @@ -58,7 +58,7 @@ type JIMMSuite struct { // Authenticator configured. JIMM *jimm.JIMM - AdminUser *dbmodel.User + AdminUser *dbmodel.Identity OFGAClient *openfga.OFGAClient COFGAClient *cofga.Client COFGAParams *cofga.OpenFGAParams @@ -88,7 +88,7 @@ func (s *JIMMSuite) SetUpTest(c *gc.C) { err = s.JIMM.Database.Migrate(ctx, false) c.Assert(err, gc.Equals, nil) - s.AdminUser = &dbmodel.User{ + s.AdminUser = &dbmodel.Identity{ Username: "alice@external", LastLogin: db.Now(), } @@ -145,7 +145,7 @@ func (s *JIMMSuite) TearDownTest(c *gc.C) { } } -func (s *JIMMSuite) NewUser(u *dbmodel.User) *openfga.User { +func (s *JIMMSuite) NewUser(u *dbmodel.Identity) *openfga.User { return openfga.NewUser(u, s.OFGAClient) } @@ -175,7 +175,7 @@ func (s *JIMMSuite) AddController(c *gc.C, name string, info *api.Info) { func (s *JIMMSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, cred jujuparams.CloudCredential) { ctx := context.Background() - u := dbmodel.User{ + u := dbmodel.Identity{ Username: tag.Owner().Id(), } user := openfga.NewUser(&u, s.JIMM.OpenFGAClient) @@ -191,7 +191,7 @@ func (s *JIMMSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, func (s *JIMMSuite) AddModel(c *gc.C, owner names.UserTag, name string, cloud names.CloudTag, region string, cred names.CloudCredentialTag) names.ModelTag { ctx := context.Background() - u := dbmodel.User{ + u := dbmodel.Identity{ Username: owner.Id(), } err := s.JIMM.Database.GetUser(ctx, &u) diff --git a/internal/jujuapi/access_control_test.go b/internal/jujuapi/access_control_test.go index fda65a63f..539eaf94b 100644 --- a/internal/jujuapi/access_control_test.go +++ b/internal/jujuapi/access_control_test.go @@ -1484,7 +1484,7 @@ func (s *accessControlSuite) TestParseTag(c *gc.C) { // TODO(ale8k): Make this an implicit thing on the JIMM suite per test & refactor the current state. // and make the suite argument an interface of the required calls we use here. func createTestControllerEnvironment(ctx context.Context, c *gc.C, s *accessControlSuite) ( - dbmodel.User, + dbmodel.Identity, dbmodel.GroupEntry, dbmodel.Controller, dbmodel.Model, @@ -1501,7 +1501,7 @@ func createTestControllerEnvironment(ctx context.Context, c *gc.C, s *accessCont err = db.GetGroup(ctx, &group) c.Assert(err, gc.IsNil) - u := dbmodel.User{ + u := dbmodel.Identity{ Username: petname.Generate(2, "-") + "@external", } c.Assert(db.DB.Create(&u).Error, gc.IsNil) diff --git a/internal/jujuapi/applicationoffers_test.go b/internal/jujuapi/applicationoffers_test.go index 0d8c52816..bb5855987 100644 --- a/internal/jujuapi/applicationoffers_test.go +++ b/internal/jujuapi/applicationoffers_test.go @@ -349,7 +349,7 @@ func (s *applicationOffersSuite) TestDestroyOffers(c *gc.C) { } err = s.JIMM.Database.GetApplicationOffer(context.Background(), &offer) c.Assert(err, gc.Equals, nil) - charlie := openfga.NewUser(&dbmodel.User{Username: "charlie@external"}, s.OFGAClient) + charlie := openfga.NewUser(&dbmodel.Identity{Username: "charlie@external"}, s.OFGAClient) err = charlie.SetApplicationOfferAccess(context.Background(), offer.ResourceTag(), ofganames.ReaderRelation) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/cloud.go b/internal/jujuapi/cloud.go index 14b523cf0..c0dd4c56f 100644 --- a/internal/jujuapi/cloud.go +++ b/internal/jujuapi/cloud.go @@ -133,7 +133,7 @@ func (r *controllerRoot) UserCredentials(ctx context.Context, userclouds jujupar results[i].Error = mapError(errors.E(op, err, errors.CodeBadRequest)) continue } - err = r.jimm.ForEachUserCloudCredential(ctx, user.User, cld, func(c *dbmodel.CloudCredential) error { + err = r.jimm.ForEachUserCloudCredential(ctx, user.Identity, cld, func(c *dbmodel.CloudCredential) error { results[i].Result = append(results[i].Result, c.Tag().String()) return nil }) @@ -169,7 +169,7 @@ func (r *controllerRoot) revokeCredential(ctx context.Context, tag string, force if err != nil { return errors.E(op, err, errors.CodeBadRequest) } - if err := r.jimm.RevokeCloudCredential(ctx, r.user.User, ct, force); err != nil { + if err := r.jimm.RevokeCloudCredential(ctx, r.user.Identity, ct, force); err != nil { return errors.E(op, err) } return nil @@ -348,7 +348,7 @@ func (r *controllerRoot) CredentialContents(ctx context.Context, args jujuparams return jujuparams.CredentialContentResults{Results: results}, nil } - err := r.jimm.ForEachUserCloudCredential(ctx, r.user.User, names.CloudTag{}, func(c *dbmodel.CloudCredential) error { + err := r.jimm.ForEachUserCloudCredential(ctx, r.user.Identity, names.CloudTag{}, func(c *dbmodel.CloudCredential) error { var result jujuparams.CredentialContentResult var err error result.Result, err = credentialContents(c) diff --git a/internal/jujuapi/cloud_test.go b/internal/jujuapi/cloud_test.go index 878ea0ab8..027d34dd7 100644 --- a/internal/jujuapi/cloud_test.go +++ b/internal/jujuapi/cloud_test.go @@ -946,13 +946,13 @@ func (s *cloudSuite) TestListCloudInfo(c *gc.C) { err = client.GrantCloud("bob@external", "add-model", "test-cloud") c.Assert(err, gc.Equals, nil) */ - bob := openfga.NewUser(&dbmodel.User{Username: "bob@external"}, s.OFGAClient) + bob := openfga.NewUser(&dbmodel.Identity{Username: "bob@external"}, s.OFGAClient) err = bob.SetCloudAccess(context.Background(), names.NewCloudTag("test-cloud"), ofganames.CanAddModelRelation) c.Assert(err, gc.Equals, nil) err = bob.SetCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), ofganames.CanAddModelRelation) c.Assert(err, gc.Equals, nil) - alice := openfga.NewUser(&dbmodel.User{Username: "alice@external"}, s.OFGAClient) + alice := openfga.NewUser(&dbmodel.Identity{Username: "alice@external"}, s.OFGAClient) err = alice.SetCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), ofganames.CanAddModelRelation) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/controller.go b/internal/jujuapi/controller.go index 422d8722c..9db1e2299 100644 --- a/internal/jujuapi/controller.go +++ b/internal/jujuapi/controller.go @@ -237,7 +237,7 @@ func (r *controllerRoot) ControllerConfig(ctx context.Context) (jujuparams.Contr }, nil } - cfg, err := r.jimm.GetControllerConfig(ctx, r.user.User) + cfg, err := r.jimm.GetControllerConfig(ctx, r.user.Identity) if err != nil { return jujuparams.ControllerConfigResult{}, errors.E(op, err) } diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index 9bbf4e679..5f5a002c2 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -43,7 +43,7 @@ type JIMM interface { ForEachCloud(ctx context.Context, user *openfga.User, f func(*dbmodel.Cloud) error) error ForEachModel(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error ForEachUserCloud(ctx context.Context, user *openfga.User, f func(*dbmodel.Cloud) error) error - ForEachUserCloudCredential(ctx context.Context, u *dbmodel.User, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error + ForEachUserCloudCredential(ctx context.Context, u *dbmodel.Identity, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error ForEachUserModel(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error FullModelStatus(ctx context.Context, user *openfga.User, modelTag names.ModelTag, patterns []string) (*jujuparams.FullStatus, error) GetApplicationOffer(ctx context.Context, user *openfga.User, offerURL string) (*jujuparams.ApplicationOfferAdminDetails, error) @@ -51,7 +51,7 @@ type JIMM interface { GetCloud(ctx context.Context, u *openfga.User, tag names.CloudTag) (dbmodel.Cloud, error) GetCloudCredential(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) GetCloudCredentialAttributes(ctx context.Context, u *openfga.User, cred *dbmodel.CloudCredential, hidden bool) (attrs map[string]string, redacted []string, err error) - GetControllerConfig(ctx context.Context, u *dbmodel.User) (*dbmodel.ControllerConfig, error) + GetControllerConfig(ctx context.Context, u *dbmodel.Identity) (*dbmodel.ControllerConfig, error) GetJimmControllerAccess(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error) GetUserCloudAccess(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error) GetUserControllerAccess(ctx context.Context, user *openfga.User, controller names.ControllerTag) (string, error) @@ -64,7 +64,7 @@ type JIMM interface { InitiateMigration(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec, targetControllerID uint) (jujuparams.InitiateMigrationResult, error) InitiateInternalMigration(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetController string) (jujuparams.InitiateMigrationResult, error) ListApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) - ModelDefaultsForCloud(ctx context.Context, user *dbmodel.User, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) + ModelDefaultsForCloud(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) ModelInfo(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelInfo, error) ModelStatus(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelStatus, error) Offer(ctx context.Context, user *openfga.User, offer jimm.AddApplicationOfferParams) error @@ -77,19 +77,19 @@ type JIMM interface { ResourceTag() names.ControllerTag RevokeAuditLogAccess(ctx context.Context, user *openfga.User, targetUserTag names.UserTag) error RevokeCloudAccess(ctx context.Context, user *openfga.User, ct names.CloudTag, ut names.UserTag, access string) error - RevokeCloudCredential(ctx context.Context, user *dbmodel.User, tag names.CloudCredentialTag, force bool) error + RevokeCloudCredential(ctx context.Context, user *dbmodel.Identity, tag names.CloudCredentialTag, force bool) error RevokeModelAccess(ctx context.Context, user *openfga.User, mt names.ModelTag, ut names.UserTag, access jujuparams.UserAccessPermission) error RevokeOfferAccess(ctx context.Context, user *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) (err error) SetControllerConfig(ctx context.Context, u *openfga.User, args jujuparams.ControllerConfigSet) error SetControllerDeprecated(ctx context.Context, user *openfga.User, controllerName string, deprecated bool) error - SetModelDefaults(ctx context.Context, user *dbmodel.User, cloudTag names.CloudTag, region string, configs map[string]interface{}) error - SetUserModelDefaults(ctx context.Context, user *dbmodel.User, configs map[string]interface{}) error - UnsetModelDefaults(ctx context.Context, user *dbmodel.User, cloudTag names.CloudTag, region string, keys []string) error + SetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, configs map[string]interface{}) error + SetUserModelDefaults(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error + UnsetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error UpdateApplicationOffer(ctx context.Context, controller *dbmodel.Controller, offerUUID string, removed bool) error UpdateCloud(ctx context.Context, u *openfga.User, ct names.CloudTag, cloud jujuparams.Cloud) error UpdateCloudCredential(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) UpdateMigratedModel(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetControllerName string) error - UserModelDefaults(ctx context.Context, user *dbmodel.User) (map[string]interface{}, error) + UserModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) ValidateModelUpgrade(ctx context.Context, u *openfga.User, mt names.ModelTag, force bool) error WatchAllModelSummaries(ctx context.Context, controller *dbmodel.Controller) (_ func() error, err error) } @@ -146,7 +146,7 @@ func (r *controllerRoot) masquerade(ctx context.Context, userTag string) (*openf if !r.user.JimmAdmin { return nil, errors.E(errors.CodeUnauthorized, "unauthorized") } - user := dbmodel.User{ + user := dbmodel.Identity{ Username: ut.Id(), } if err := r.jimm.DB().GetUser(ctx, &user); err != nil { diff --git a/internal/jujuapi/jimm_test.go b/internal/jujuapi/jimm_test.go index a73164034..efb468b80 100644 --- a/internal/jujuapi/jimm_test.go +++ b/internal/jujuapi/jimm_test.go @@ -567,7 +567,7 @@ func (s *jimmSuite) TestImportModel(c *gc.C) { func (s *jimmSuite) TestAddCloudToController(c *gc.C) { ctx := context.Background() - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } err := s.JIMM.Database.GetUser(ctx, &u) @@ -604,7 +604,7 @@ func (s *jimmSuite) TestAddCloudToController(c *gc.C) { func (s *jimmSuite) TestAddExistingCloudToController(c *gc.C) { ctx := context.Background() - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } err := s.JIMM.Database.GetUser(ctx, &u) @@ -652,7 +652,7 @@ func (s *jimmSuite) TestAddExistingCloudToController(c *gc.C) { func (s *jimmSuite) TestRemoveCloudFromController(c *gc.C) { ctx := context.Background() - u := dbmodel.User{ + u := dbmodel.Identity{ Username: "alice@external", } err := s.JIMM.Database.GetUser(ctx, &u) diff --git a/internal/jujuapi/modelmanager.go b/internal/jujuapi/modelmanager.go index c4096e83a..31f9aef39 100644 --- a/internal/jujuapi/modelmanager.go +++ b/internal/jujuapi/modelmanager.go @@ -318,7 +318,7 @@ func (r *controllerRoot) SetModelDefaults(ctx context.Context, args jujuparams.S results[i].Error = mapError(errors.E(op, err)) continue } - results[i].Error = mapError(r.jimm.SetModelDefaults(ctx, r.user.User, cloudTag, config.CloudRegion, config.Config)) + results[i].Error = mapError(r.jimm.SetModelDefaults(ctx, r.user.Identity, cloudTag, config.CloudRegion, config.Config)) } return jujuparams.ErrorResults{ @@ -335,7 +335,7 @@ func (r *controllerRoot) UnsetModelDefaults(ctx context.Context, args jujuparams results[i].Error = mapError(err) continue } - results[i].Error = mapError(r.jimm.UnsetModelDefaults(ctx, r.user.User, cloudTag, key.CloudRegion, key.Keys)) + results[i].Error = mapError(r.jimm.UnsetModelDefaults(ctx, r.user.Identity, cloudTag, key.CloudRegion, key.Keys)) } return jujuparams.ErrorResults{ @@ -356,7 +356,7 @@ func (r *controllerRoot) ModelDefaultsForClouds(ctx context.Context, args jujupa result.Results[i].Error = mapError(errors.E(op, err)) continue } - defaults, err := r.jimm.ModelDefaultsForCloud(ctx, r.user.User, cloudTag) + defaults, err := r.jimm.ModelDefaultsForCloud(ctx, r.user.Identity, cloudTag) if err != nil { result.Results[i].Error = mapError(errors.E(op, err)) continue diff --git a/internal/jujuapi/modelmanager_test.go b/internal/jujuapi/modelmanager_test.go index 4135653bf..e3f06c0f4 100644 --- a/internal/jujuapi/modelmanager_test.go +++ b/internal/jujuapi/modelmanager_test.go @@ -264,7 +264,7 @@ func (s *modelManagerSuite) TestModelInfo(c *gc.C) { //client := modelmanager.NewClient(conn) //err := client.GrantModel("bob@external", "write", mt4.Id()) //c.Assert(err, gc.Equals, nil) - bob := openfga.NewUser(&dbmodel.User{Username: "bob@external"}, s.OFGAClient) + bob := openfga.NewUser(&dbmodel.Identity{Username: "bob@external"}, s.OFGAClient) err := bob.SetModelAccess(context.Background(), mt4, ofganames.WriterRelation) c.Assert(err, gc.Equals, nil) @@ -521,7 +521,7 @@ func (s *modelManagerSuite) TestModelInfoDisableControllerUUIDMasking(c *gc.C) { s.Candid.AddUser("bob", "controller-admin") // we make bob a jimm administrator - bob := openfga.NewUser(&dbmodel.User{Username: "bob@external"}, s.OFGAClient) + bob := openfga.NewUser(&dbmodel.Identity{Username: "bob@external"}, s.OFGAClient) err = bob.SetControllerAccess(context.Background(), s.JIMM.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/websocket_test.go b/internal/jujuapi/websocket_test.go index 6fab801d1..478da9dc8 100644 --- a/internal/jujuapi/websocket_test.go +++ b/internal/jujuapi/websocket_test.go @@ -87,7 +87,7 @@ func (s *websocketSuite) SetUpTest(c *gc.C) { c.Assert(err, gc.Equals, nil) bob := openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: "bob@external", }, s.OFGAClient, @@ -175,7 +175,7 @@ func (s *proxySuite) TestConnectToModel(c *gc.C) { func (s *proxySuite) TestConnectToModelAndLogin(c *gc.C) { ctx := context.Background() alice := names.NewUserTag("alice") - aliceUser := openfga.NewUser(&dbmodel.User{Username: alice.Id()}, s.JIMM.OpenFGAClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Username: alice.Id()}, s.JIMM.OpenFGAClient) err := aliceUser.SetControllerAccess(ctx, s.Model.Controller.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, gc.IsNil) conn, err := s.openNoAssert(c, &api.Info{ diff --git a/internal/openfga/user.go b/internal/openfga/user.go index af531c49c..34d185329 100644 --- a/internal/openfga/user.go +++ b/internal/openfga/user.go @@ -18,17 +18,17 @@ import ( // NewUser returns a new user structure that can be used to check // user's access rights to various resources. -func NewUser(u *dbmodel.User, client *OFGAClient) *User { +func NewUser(u *dbmodel.Identity, client *OFGAClient) *User { return &User{ - User: u, - client: client, + Identity: u, + client: client, } } // User wraps dbmodel.User and implements methods that enable us // to check user's access rights to various resources. type User struct { - *dbmodel.User + *dbmodel.Identity client *OFGAClient JimmAdmin bool } @@ -440,7 +440,7 @@ func ListUsersWithAccess[T ofganames.ResourceTagger](ctx context.Context, client if entity.ID == "*" { entity.ID = ofganames.EveryoneUser } - users[i] = NewUser(&dbmodel.User{Username: entity.ID}, client) + users[i] = NewUser(&dbmodel.Identity{Username: entity.ID}, client) } return users, nil } diff --git a/internal/openfga/user_test.go b/internal/openfga/user_test.go index 8b9b4f2b8..e4a14670e 100644 --- a/internal/openfga/user_test.go +++ b/internal/openfga/user_test.go @@ -54,7 +54,7 @@ func (s *userTestSuite) TestIsAdministrator(c *gc.C) { c.Assert(err, gc.IsNil) u := openfga.NewUser( - &dbmodel.User{ + &dbmodel.Identity{ Username: user.Id(), }, s.ofgaClient, @@ -102,9 +102,9 @@ func (s *userTestSuite) TestModelAccess(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.User{Username: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.User{Username: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.User{Username: alice.Id()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Username: "adam"}, s.ofgaClient) + eveUser := openfga.NewUser(&dbmodel.Identity{Username: eve.Id()}, s.ofgaClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Username: alice.Id()}, s.ofgaClient) relation := eveUser.GetModelAccess(ctx, model) c.Assert(relation, gc.DeepEquals, ofganames.AdministratorRelation) @@ -137,9 +137,9 @@ func (s *userTestSuite) TestSetModelAccess(c *gc.C) { eve := names.NewUserTag("eve") alice := names.NewUserTag("alice") - adamUser := openfga.NewUser(&dbmodel.User{Username: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.User{Username: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.User{Username: alice.Id()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Username: "adam"}, s.ofgaClient) + eveUser := openfga.NewUser(&dbmodel.Identity{Username: eve.Id()}, s.ofgaClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Username: alice.Id()}, s.ofgaClient) err = eveUser.SetModelAccess(ctx, model, ofganames.AdministratorRelation) c.Assert(err, gc.IsNil) @@ -197,9 +197,9 @@ func (s *userTestSuite) TestCloudAccess(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.User{Username: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.User{Username: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.User{Username: alice.Id()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Username: "adam"}, s.ofgaClient) + eveUser := openfga.NewUser(&dbmodel.Identity{Username: eve.Id()}, s.ofgaClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Username: alice.Id()}, s.ofgaClient) relation := eveUser.GetCloudAccess(ctx, cloud) c.Assert(relation, gc.DeepEquals, ofganames.AdministratorRelation) @@ -220,9 +220,9 @@ func (s *userTestSuite) TestSetCloudAccess(c *gc.C) { eve := names.NewUserTag("eve") alice := names.NewUserTag("alice") - adamUser := openfga.NewUser(&dbmodel.User{Username: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.User{Username: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.User{Username: alice.Id()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Username: "adam"}, s.ofgaClient) + eveUser := openfga.NewUser(&dbmodel.Identity{Username: eve.Id()}, s.ofgaClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Username: alice.Id()}, s.ofgaClient) err = eveUser.SetCloudAccess(ctx, cloud, ofganames.AdministratorRelation) c.Assert(err, gc.IsNil) @@ -273,9 +273,9 @@ func (s *userTestSuite) TestControllerAccess(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.User{Username: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.User{Username: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.User{Username: alice.Id()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Username: "adam"}, s.ofgaClient) + eveUser := openfga.NewUser(&dbmodel.Identity{Username: eve.Id()}, s.ofgaClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Username: alice.Id()}, s.ofgaClient) relation := eveUser.GetControllerAccess(ctx, controller) c.Assert(relation, gc.DeepEquals, ofganames.AdministratorRelation) @@ -302,9 +302,9 @@ func (s *userTestSuite) TestSetControllerAccess(c *gc.C) { eve := names.NewUserTag("eve") alice := names.NewUserTag("alice") - adamUser := openfga.NewUser(&dbmodel.User{Username: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.User{Username: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.User{Username: alice.Id()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Username: "adam"}, s.ofgaClient) + eveUser := openfga.NewUser(&dbmodel.Identity{Username: eve.Id()}, s.ofgaClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Username: alice.Id()}, s.ofgaClient) err = eveUser.SetControllerAccess(ctx, controller, ofganames.AdministratorRelation) c.Assert(err, gc.IsNil) @@ -339,10 +339,10 @@ func (s *userTestSuite) TestUnsetAuditLogViewerAccess(c *gc.C) { c.Assert(err, gc.IsNil) controller := names.NewControllerTag(controllerUUID.String()) - aliceUser := openfga.NewUser(&dbmodel.User{Username: "alice"}, s.ofgaClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Username: "alice"}, s.ofgaClient) tuples := []openfga.Tuple{{ - Object: ofganames.ConvertTag(aliceUser.User.ResourceTag()), + Object: ofganames.ConvertTag(aliceUser.Identity.ResourceTag()), Relation: ofganames.AuditLogViewerRelation, Target: ofganames.ConvertTag(controller), }} @@ -425,7 +425,7 @@ func (s *userTestSuite) TestListRelatedUsers(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - eveUser := openfga.NewUser(&dbmodel.User{Username: "eve"}, s.ofgaClient) + eveUser := openfga.NewUser(&dbmodel.Identity{Username: "eve"}, s.ofgaClient) isAdministrator, err := openfga.IsAdministrator(ctx, eveUser, offer) c.Assert(err, gc.IsNil) c.Assert(isAdministrator, gc.Equals, true) @@ -473,7 +473,7 @@ func (s *userTestSuite) TestListModels(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.User{Username: adam.Name()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Username: adam.Name()}, s.ofgaClient) modelUUIDs, err := adamUser.ListModels(ctx, ofganames.ReaderRelation) c.Assert(err, gc.IsNil) wantUUIDs := []string{model1UUID.String(), model2UUID.String(), model3UUID.String()} @@ -515,7 +515,7 @@ func (s *userTestSuite) TestListApplicationOffers(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.User{Username: adam.Name()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Username: adam.Name()}, s.ofgaClient) offerUUIDs, err := adamUser.ListApplicationOffers(ctx, ofganames.ReaderRelation) c.Assert(err, gc.IsNil) wantUUIDs := []string{offer1UUID.String(), offer2UUID.String(), offer3UUID.String()} diff --git a/local/seed_db/main.go b/local/seed_db/main.go index ddef8052f..dd180b8a3 100644 --- a/local/seed_db/main.go +++ b/local/seed_db/main.go @@ -47,7 +47,7 @@ func main() { os.Exit(1) } - u := dbmodel.User{ + u := dbmodel.Identity{ Username: petname.Generate(2, "-") + "@external", } if err = db.DB.Create(&u).Error; err != nil { diff --git a/service.go b/service.go index 02c6facfd..b455431f4 100644 --- a/service.go +++ b/service.go @@ -557,7 +557,7 @@ func ensureControllerAdministrators(ctx context.Context, client *openfga.OFGACli tuples := []openfga.Tuple{} for _, username := range admins { userTag := names.NewUserTag(username) - user := openfga.NewUser(&dbmodel.User{Username: userTag.Id()}, client) + user := openfga.NewUser(&dbmodel.Identity{Username: userTag.Id()}, client) isAdmin, err := openfga.IsAdministrator(ctx, user, controller) if err != nil { return errors.E(err) diff --git a/service_test.go b/service_test.go index 5827e6a16..ab0a2dc19 100644 --- a/service_test.go +++ b/service_test.go @@ -252,7 +252,7 @@ func TestOpenFGA(t *testing.T) { // assert controller admins have been created in openfga for _, username := range []string{"alice", "eve"} { user := openfga.NewUser( - &dbmodel.User{Username: username}, + &dbmodel.Identity{Username: username}, client, ) allowed, err := openfga.IsAdministrator(context.Background(), user, names.NewControllerTag(p.ControllerUUID)) @@ -296,7 +296,7 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { UUID: "7e4e7ffb-5116-4544-a400-f584d08c410e", Name: "test-application-offer", } - user := dbmodel.User{ + user := dbmodel.Identity{ Username: "alice@external", } @@ -304,7 +304,7 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { tests := []struct { about string - setup func(c *qt.C, ofgaClient *openfga.OFGAClient, user *dbmodel.User) + setup func(c *qt.C, ofgaClient *openfga.OFGAClient, user *dbmodel.Identity) caveats []string expectDeclared map[string]string expectedError string @@ -314,7 +314,7 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { expectedError: ".*third party refused discharge: cannot discharge: caveat not recognized", }, { about: "user is an offer reader", - setup: func(c *qt.C, ofgaClient *openfga.OFGAClient, user *dbmodel.User) { + setup: func(c *qt.C, ofgaClient *openfga.OFGAClient, user *dbmodel.Identity) { u := openfga.NewUser(user, ofgaClient) err := u.SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.ReaderRelation) c.Assert(err, qt.IsNil) @@ -327,7 +327,7 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { expectedError: ".*cannot discharge: permission denied", }, { about: "user is an offer consumer", - setup: func(c *qt.C, ofgaClient *openfga.OFGAClient, user *dbmodel.User) { + setup: func(c *qt.C, ofgaClient *openfga.OFGAClient, user *dbmodel.Identity) { u := openfga.NewUser(user, ofgaClient) err := u.SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.ConsumerRelation) c.Assert(err, qt.IsNil) @@ -336,7 +336,7 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { expectDeclared: map[string]string{"offer-uuid": offer.UUID}, }, { about: "user is an offer administrator", - setup: func(c *qt.C, ofgaClient *openfga.OFGAClient, user *dbmodel.User) { + setup: func(c *qt.C, ofgaClient *openfga.OFGAClient, user *dbmodel.Identity) { u := openfga.NewUser(user, ofgaClient) err := u.SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) From b87010ce05f0f9422bacaa9e46962b2e233a9aa5 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Jan 2024 14:00:35 +0000 Subject: [PATCH 006/126] Update `Identity` godoc Signed-off-by: Babak K. Shandiz --- internal/dbmodel/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/dbmodel/user.go b/internal/dbmodel/user.go index 81421f440..442fc2834 100644 --- a/internal/dbmodel/user.go +++ b/internal/dbmodel/user.go @@ -10,7 +10,7 @@ import ( "gorm.io/gorm" ) -// A Identity represents a JIMM user. +// A Identity represents a JIMM identity, which can be a user or a service account. type Identity struct { gorm.Model From 852c2837c75e598b9e3963b606b535243ea72665 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Jan 2024 19:51:27 +0000 Subject: [PATCH 007/126] Rename `identity_name` column to `identity` Signed-off-by: Babak K. Shandiz --- internal/dbmodel/sql/postgres/1_6.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql index 905290acc..fe8161880 100644 --- a/internal/dbmodel/sql/postgres/1_6.sql +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -5,8 +5,8 @@ -- "When renaming a constraint that has an underlying index, the index is renamed as well." -- (See https://www.postgresql.org/docs/current/sql-altertable.html) +ALTER TABLE IF EXISTS users RENAME COLUMN username TO name; ALTER TABLE IF EXISTS users RENAME TO identities; -ALTER TABLE IF EXISTS users RENAME COLUMN username TO identity_name; ALTER TABLE IF EXISTS cloud_credentials RENAME COLUMN owner_username TO owner_identity_name; ALTER TABLE IF EXISTS cloud_defaults RENAME COLUMN username TO identity_name; From a89e77d8dbff0a2732e7af9bab60461a3ec528d3 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Jan 2024 19:52:53 +0000 Subject: [PATCH 008/126] Rename `Identity.Username` to `Identity.Name` Signed-off-by: Babak K. Shandiz --- cmd/jimmctl/cmd/addcloudtocontroller_test.go | 4 +- cmd/jimmctl/cmd/jimmsuite_test.go | 6 +- cmd/jimmctl/cmd/relation_test.go | 22 ++-- discharger.go | 4 +- internal/auth/jujuauth.go | 2 +- internal/auth/jujuauth_test.go | 6 +- internal/db/applicationoffer_test.go | 4 +- internal/db/cloudcredential_test.go | 22 ++-- internal/db/clouddefaults.go | 2 +- internal/db/clouddefaults_test.go | 12 +- internal/db/controller_test.go | 2 +- internal/db/db_test.go | 2 +- internal/db/model_test.go | 24 ++-- internal/db/user.go | 12 +- internal/db/user_test.go | 16 +-- internal/db/usermodeldefaults_test.go | 14 +-- internal/dbmodel/cloudcredential_test.go | 6 +- internal/dbmodel/controller_test.go | 2 +- internal/dbmodel/model.go | 4 +- internal/dbmodel/model_test.go | 6 +- internal/dbmodel/user.go | 52 ++++----- internal/dbmodel/user_test.go | 10 +- internal/jimm/access_test.go | 6 +- internal/jimm/applicationoffer.go | 18 +-- internal/jimm/applicationoffer_test.go | 88 +++++++------- internal/jimm/cloud.go | 6 +- internal/jimm/cloud_test.go | 20 ++-- internal/jimm/cloudcredential.go | 10 +- internal/jimm/cloudcredential_test.go | 64 +++++----- internal/jimm/clouddefaults.go | 4 +- internal/jimm/clouddefaults_test.go | 44 +++---- internal/jimm/controller.go | 8 +- internal/jimm/controller_test.go | 24 ++-- internal/jimm/jimm_test.go | 6 +- internal/jimm/model.go | 18 +-- internal/jimm/model_test.go | 14 +-- internal/jimm/user.go | 2 +- internal/jimm/user_test.go | 10 +- internal/jimm/usermodeldefaults.go | 4 +- internal/jimm/usermodeldefaults_test.go | 18 +-- internal/jimmtest/env.go | 8 +- internal/jimmtest/suite.go | 6 +- internal/jujuapi/access_control.go | 2 +- internal/jujuapi/access_control_test.go | 116 +++++++++---------- internal/jujuapi/applicationoffers.go | 2 +- internal/jujuapi/applicationoffers_test.go | 2 +- internal/jujuapi/cloud.go | 2 +- internal/jujuapi/cloud_test.go | 4 +- internal/jujuapi/controllerroot.go | 2 +- internal/jujuapi/jimm_test.go | 6 +- internal/jujuapi/modelmanager_test.go | 4 +- internal/jujuapi/usermanager.go | 2 +- internal/jujuapi/websocket_test.go | 4 +- internal/openfga/user.go | 4 +- internal/openfga/user_test.go | 46 ++++---- local/seed_db/main.go | 8 +- service.go | 2 +- service_test.go | 4 +- 58 files changed, 411 insertions(+), 411 deletions(-) diff --git a/cmd/jimmctl/cmd/addcloudtocontroller_test.go b/cmd/jimmctl/cmd/addcloudtocontroller_test.go index 602983535..4603d5a5e 100644 --- a/cmd/jimmctl/cmd/addcloudtocontroller_test.go +++ b/cmd/jimmctl/cmd/addcloudtocontroller_test.go @@ -33,7 +33,7 @@ func (s *addCloudToControllerSuite) SetUpTest(c *gc.C) { // We add user bob, who is a JIMM administrator. err := s.JIMM.Database.UpdateUser(context.Background(), &dbmodel.Identity{ DisplayName: "Bob", - Username: "bob@external", + Name: "bob@external", }) c.Assert(err, gc.IsNil) @@ -52,7 +52,7 @@ func (s *addCloudToControllerSuite) SetUpTest(c *gc.C) { // test-cloud. bob := openfga.NewUser( &dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", }, s.JIMM.OpenFGAClient, ) diff --git a/cmd/jimmctl/cmd/jimmsuite_test.go b/cmd/jimmctl/cmd/jimmsuite_test.go index c2918da6b..f75a3e026 100644 --- a/cmd/jimmctl/cmd/jimmsuite_test.go +++ b/cmd/jimmctl/cmd/jimmsuite_test.go @@ -104,7 +104,7 @@ func (s *jimmSuite) SetUpTest(c *gc.C) { s.JujuConnSuite.SetUpTest(c) s.AdminUser = &dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", LastLogin: db.Now(), } err = s.JIMM.Database.GetUser(ctx, s.AdminUser) @@ -208,7 +208,7 @@ func (s *jimmSuite) AddController(c *gc.C, name string, info *api.Info) { func (s *jimmSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, cred jujuparams.CloudCredential) { ctx := context.Background() u := dbmodel.Identity{ - Username: tag.Owner().Id(), + Name: tag.Owner().Id(), } user := openfga.NewUser(&u, s.JIMM.OpenFGAClient) err := s.JIMM.Database.GetUser(ctx, &u) @@ -225,7 +225,7 @@ func (s *jimmSuite) AddModel(c *gc.C, owner names.UserTag, name string, cloud na ctx := context.Background() u := openfga.NewUser( &dbmodel.Identity{ - Username: owner.Id(), + Name: owner.Id(), }, s.OFGAClient, ) diff --git a/cmd/jimmctl/cmd/relation_test.go b/cmd/jimmctl/cmd/relation_test.go index a5c24c80e..89324ede5 100644 --- a/cmd/jimmctl/cmd/relation_test.go +++ b/cmd/jimmctl/cmd/relation_test.go @@ -267,7 +267,7 @@ func initializeEnvironment(c *gc.C, ctx context.Context, db *db.Database, u dbmo env := environment{} u1 := dbmodel.Identity{ - Username: "eve@external", + Name: "eve@external", } c.Assert(db.DB.Create(&u1).Error, gc.IsNil) @@ -302,7 +302,7 @@ func initializeEnvironment(c *gc.C, ctx context.Context, db *db.Database, u dbmo cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) @@ -315,7 +315,7 @@ func initializeEnvironment(c *gc.C, ctx context.Context, db *db.Database, u dbmo String: "acdbf3e5-67e1-42a2-a2dc-64505265c030", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -353,11 +353,11 @@ func (s *relationSuite) TestListRelations(c *gc.C) { } relations := []apiparams.RelationshipTuple{{ - Object: "user-" + env.users[0].Username, + Object: "user-" + env.users[0].Name, Relation: "member", TargetObject: "group-group-1", }, { - Object: "user-" + env.users[1].Username, + Object: "user-" + env.users[1].Name, Relation: "member", TargetObject: "group-group-2", }, { @@ -373,7 +373,7 @@ func (s *relationSuite) TestListRelations(c *gc.C) { Relation: "administrator", TargetObject: "model-" + env.controllers[0].Name + ":" + env.models[0].OwnerUsername + "/" + env.models[0].Name, }, { - Object: "user-" + env.users[1].Username, + Object: "user-" + env.users[1].Name, Relation: "administrator", TargetObject: "applicationoffer-" + env.controllers[0].Name + ":" + env.applicationOffers[0].Model.OwnerUsername + "/" + env.applicationOffers[0].Model.Name + "." + env.applicationOffers[0].Name, }} @@ -449,7 +449,7 @@ func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { c.Assert(err, gc.IsNil) u := dbmodel.Identity{ - Username: petname.Generate(2, "-") + "@external", + Name: petname.Generate(2, "-") + "@external", } c.Assert(db.DB.Create(&u).Error, gc.IsNil) @@ -478,7 +478,7 @@ func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { cred := dbmodel.CloudCredential{ Name: petname.Generate(2, "-"), CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) @@ -490,7 +490,7 @@ func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { String: id.String(), Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -522,8 +522,8 @@ func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { c.Assert(err, gc.IsNil) // Test reader is OK - userToCheck := "user-" + u.Username - modelToCheck := "model-" + controller.Name + ":" + u.Username + "/" + model.Name + userToCheck := "user-" + u.Name + modelToCheck := "model-" + controller.Name + ":" + u.Name + "/" + model.Name cmdCtx, err := cmdtesting.RunCommand( c, cmd.NewCheckRelationCommandForTesting(s.ClientStore(), bClient), diff --git a/discharger.go b/discharger.go index e6bf6706d..e3ce30274 100644 --- a/discharger.go +++ b/discharger.go @@ -107,7 +107,7 @@ func (md *macaroonDischarger) checkThirdPartyCaveat(ctx context.Context, req *ht user := openfga.NewUser( &dbmodel.Identity{ - Username: userTag.Id(), + Name: userTag.Id(), }, md.ofgaClient, ) @@ -124,6 +124,6 @@ func (md *macaroonDischarger) checkThirdPartyCaveat(ctx context.Context, req *ht checkers.TimeBeforeCaveat(time.Now().Add(defaultDischargeExpiry)), }, nil } - zapctx.Debug(ctx, "macaroon dishcharge denied", zap.String("user", user.Username), zap.String("offer", offerUUID)) + zapctx.Debug(ctx, "macaroon dishcharge denied", zap.String("user", user.Name), zap.String("offer", offerUUID)) return nil, httpbakery.ErrPermissionDenied } diff --git a/internal/auth/jujuauth.go b/internal/auth/jujuauth.go index 619938927..97e0fac60 100644 --- a/internal/auth/jujuauth.go +++ b/internal/auth/jujuauth.go @@ -72,7 +72,7 @@ func (a JujuAuthenticator) Authenticate(ctx context.Context, req *jujuparams.Log ut = ut.WithDomain("external") } u := &dbmodel.Identity{ - Username: ut.Id(), + Name: ut.Id(), DisplayName: ut.Name(), } // Note: Previously here we would grant a user superuser permission if they were part of diff --git a/internal/auth/jujuauth_test.go b/internal/auth/jujuauth_test.go index 109888a80..73620ce50 100644 --- a/internal/auth/jujuauth_test.go +++ b/internal/auth/jujuauth_test.go @@ -60,7 +60,7 @@ func TestAuthenticateLogin(t *testing.T) { c.Check(u.LastLogin.Valid, qt.Equals, false) u.LastLogin = sql.NullTime{} c.Check(u.Identity, qt.DeepEquals, &dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", DisplayName: "alice", }) } @@ -103,7 +103,7 @@ func TestAuthenticateLoginWithDomain(t *testing.T) { c.Check(u.LastLogin.Valid, qt.Equals, false) u.LastLogin = sql.NullTime{} c.Check(u.Identity, qt.DeepEquals, &dbmodel.Identity{ - Username: "alice@mydomain", + Name: "alice@mydomain", DisplayName: "alice", }) } @@ -147,7 +147,7 @@ func TestAuthenticateLoginSuperuser(t *testing.T) { c.Check(u.LastLogin.Valid, qt.Equals, false) u.LastLogin = sql.NullTime{} c.Check(u.Identity, qt.DeepEquals, &dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", DisplayName: "bob", }) } diff --git a/internal/db/applicationoffer_test.go b/internal/db/applicationoffer_test.go index 1ec3485c7..752c30dd6 100644 --- a/internal/db/applicationoffer_test.go +++ b/internal/db/applicationoffer_test.go @@ -41,7 +41,7 @@ func initTestEnvironment(c *qt.C, db *db.Database) testEnvironment { env := testEnvironment{} env.u = dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(db.DB.Create(&env.u).Error, qt.IsNil) @@ -242,7 +242,7 @@ func (s *dbSuite) TestFindApplicationOffers(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) diff --git a/internal/db/cloudcredential_test.go b/internal/db/cloudcredential_test.go index f2d55bc56..77ae3778e 100644 --- a/internal/db/cloudcredential_test.go +++ b/internal/db/cloudcredential_test.go @@ -31,7 +31,7 @@ func (s *dbSuite) TestSetCloudCredentialInvalidTag(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -46,7 +46,7 @@ func (s *dbSuite) TestSetCloudCredentialInvalidTag(c *qt.C) { cred := dbmodel.CloudCredential{ Name: "test-cred", - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = s.Database.SetCloudCredential(context.Background(), &cred) @@ -59,7 +59,7 @@ func (s *dbSuite) TestSetCloudCredential(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -75,7 +75,7 @@ func (s *dbSuite) TestSetCloudCredential(c *qt.C) { cred := dbmodel.CloudCredential{ Name: "test-cred", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } c1 := cred @@ -83,7 +83,7 @@ func (s *dbSuite) TestSetCloudCredential(c *qt.C) { c.Assert(err, qt.Equals, nil) var dbCred dbmodel.CloudCredential - result := s.Database.DB.Where("cloud_name = ? AND owner_username = ? AND name = ?", cloud.Name, u.Username, cred.Name).First(&dbCred) + result := s.Database.DB.Where("cloud_name = ? AND owner_username = ? AND name = ?", cloud.Name, u.Name, cred.Name).First(&dbCred) c.Assert(result.Error, qt.Equals, nil) c.Assert(dbCred, qt.DeepEquals, cred) @@ -96,7 +96,7 @@ func (s *dbSuite) TestSetCloudCredentialUpdate(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -112,7 +112,7 @@ func (s *dbSuite) TestSetCloudCredentialUpdate(c *qt.C) { cred := dbmodel.CloudCredential{ Name: "test-cred", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = s.Database.SetCloudCredential(context.Background(), &cred) @@ -136,7 +136,7 @@ func (s *dbSuite) TestSetCloudCredentialUpdate(c *qt.C) { dbCred := dbmodel.CloudCredential{ CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, Name: cred.Name, } err = s.Database.GetCloudCredential(context.Background(), &dbCred) @@ -166,7 +166,7 @@ func (s *dbSuite) TestGetCloudCredential(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -182,7 +182,7 @@ func (s *dbSuite) TestGetCloudCredential(c *qt.C) { cred := dbmodel.CloudCredential{ Name: "test-cred", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } cred.Cloud.Regions = nil @@ -194,7 +194,7 @@ func (s *dbSuite) TestGetCloudCredential(c *qt.C) { dbCred := dbmodel.CloudCredential{ CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, Name: cred.Name, } err = s.Database.GetCloudCredential(context.Background(), &dbCred) diff --git a/internal/db/clouddefaults.go b/internal/db/clouddefaults.go index 8d7ded083..cba862111 100644 --- a/internal/db/clouddefaults.go +++ b/internal/db/clouddefaults.go @@ -143,7 +143,7 @@ func (d *Database) ModelDefaultsForCloud(ctx context.Context, user *dbmodel.Iden } db := d.DB.WithContext(ctx) - db = db.Where("username = ?", user.Username) + db = db.Where("username = ?", user.Name) db = db.Joins("JOIN clouds ON clouds.id = cloud_defaults.cloud_id") db = db.Where("clouds.name = ?", cloud.Id()) diff --git a/internal/db/clouddefaults_test.go b/internal/db/clouddefaults_test.go index 368bb4365..4fad49246 100644 --- a/internal/db/clouddefaults_test.go +++ b/internal/db/clouddefaults_test.go @@ -23,7 +23,7 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -48,7 +48,7 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { cloud := cloud1 cloud.Regions = nil defaults := dbmodel.CloudDefaults{ - Username: u.Username, + Username: u.Name, User: u, CloudID: cloud.ID, Cloud: cloud, @@ -81,7 +81,7 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { }) dbDefaults := dbmodel.CloudDefaults{ - Username: u.Username, + Username: u.Name, CloudID: cloud2.ID, Cloud: cloud2, Region: cloud2.Regions[0].Name, @@ -91,7 +91,7 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { c.Assert(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) dbDefaults = dbmodel.CloudDefaults{ - Username: u.Username, + Username: u.Name, CloudID: cloud1.ID, Cloud: cloud1, Region: cloud1.Regions[0].Name, @@ -106,7 +106,7 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { err = s.Database.CloudDefaults(ctx, &dbDefaults) c.Assert(err, qt.Equals, nil) c.Assert(dbDefaults, qt.CmpEquals(cmpopts.IgnoreTypes([]dbmodel.CloudRegion{}, gorm.Model{})), dbmodel.CloudDefaults{ - Username: u.Username, + Username: u.Name, User: u, CloudID: cloud1.ID, Cloud: cloud1, @@ -117,7 +117,7 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { }) err = s.Database.UnsetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: u.Username, + Username: u.Name, CloudID: cloud2.ID, Region: "no-such-region", }, []string{"key1", "key2", "unknown-key"}) diff --git a/internal/db/controller_test.go b/internal/db/controller_test.go index 17e54ce1e..df6caea9f 100644 --- a/internal/db/controller_test.go +++ b/internal/db/controller_test.go @@ -123,7 +123,7 @@ func (s *dbSuite) TestGetControllerWithModels(c *qt.C) { CloudRegion: "test-region", } u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) diff --git a/internal/db/db_test.go b/internal/db/db_test.go index bd714d095..ca0df08ee 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -62,7 +62,7 @@ func (s *dbSuite) TestTransaction(c *qt.C) { err = s.Database.Transaction(func(d *db.Database) error { c.Check(d, qt.Not(qt.Equals), s.Database) - return d.GetUser(context.Background(), &dbmodel.Identity{Username: "bob@external"}) + return d.GetUser(context.Background(), &dbmodel.Identity{Name: "bob@external"}) }) c.Assert(err, qt.IsNil) diff --git a/internal/db/model_test.go b/internal/db/model_test.go index aa573f2b2..599d65e96 100644 --- a/internal/db/model_test.go +++ b/internal/db/model_test.go @@ -32,7 +32,7 @@ func (s *dbSuite) TestAddModel(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -69,7 +69,7 @@ func (s *dbSuite) TestAddModel(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -105,7 +105,7 @@ func (s *dbSuite) TestGetModel(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -142,7 +142,7 @@ func (s *dbSuite) TestGetModel(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, Owner: u, ControllerID: controller.ID, Controller: controller, @@ -205,7 +205,7 @@ func (s *dbSuite) TestUpdateModel(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -238,7 +238,7 @@ func (s *dbSuite) TestUpdateModel(c *qt.C) { model := dbmodel.Model{ Name: "test-model-1", - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -283,7 +283,7 @@ func (s *dbSuite) TestDeleteModel(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -316,7 +316,7 @@ func (s *dbSuite) TestDeleteModel(c *qt.C) { model := dbmodel.Model{ Name: "test-model-1", - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, Controller: controller, CloudRegionID: cloud.Regions[0].ID, @@ -357,7 +357,7 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -401,7 +401,7 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred1.ID, @@ -425,7 +425,7 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000002", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred2.ID, @@ -454,7 +454,7 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, Controller: controller, CloudRegionID: cloud.Regions[0].ID, diff --git a/internal/db/user.go b/internal/db/user.go index 230fc3cee..4a3276e3a 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -25,12 +25,12 @@ func (d *Database) GetUser(ctx context.Context, u *dbmodel.Identity) error { return errors.E(op, err) } - if u.Username == "" { + if u.Name == "" { return errors.E(op, errors.CodeNotFound, `invalid username ""`) } db := d.DB.WithContext(ctx) - if err := db.Where("username = ?", u.Username).FirstOrCreate(&u).Error; err != nil { + if err := db.Where("username = ?", u.Name).FirstOrCreate(&u).Error; err != nil { return errors.E(op, err) } return nil @@ -46,12 +46,12 @@ func (d *Database) FetchUser(ctx context.Context, u *dbmodel.Identity) error { return errors.E(op, err) } - if u.Username == "" { + if u.Name == "" { return errors.E(op, errors.CodeNotFound, `invalid username ""`) } db := d.DB.WithContext(ctx) - if err := db.Where("username = ?", u.Username).First(&u).Error; err != nil { + if err := db.Where("username = ?", u.Name).First(&u).Error; err != nil { return errors.E(op, err) } return nil @@ -69,7 +69,7 @@ func (d *Database) UpdateUser(ctx context.Context, u *dbmodel.Identity) error { return errors.E(op, err) } - if u.Username == "" { + if u.Name == "" { return errors.E(op, errors.CodeNotFound, `invalid username ""`) } @@ -88,7 +88,7 @@ func (d *Database) GetUserCloudCredentials(ctx context.Context, u *dbmodel.Ident return nil, errors.E(op, err) } - if u.Username == "" || cloud == "" { + if u.Name == "" || cloud == "" { return nil, errors.E(op, errors.CodeNotFound, `cloudcredential not found`) } diff --git a/internal/db/user_test.go b/internal/db/user_test.go index 8c47372db..93b09f27d 100644 --- a/internal/db/user_test.go +++ b/internal/db/user_test.go @@ -36,13 +36,13 @@ func (s *dbSuite) TestGetUser(c *qt.C) { c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } err = s.Database.GetUser(ctx, &u) c.Assert(err, qt.IsNil) u2 := dbmodel.Identity{ - Username: u.Username, + Name: u.Name, } err = s.Database.GetUser(ctx, &u2) c.Assert(err, qt.IsNil) @@ -72,7 +72,7 @@ func (s *dbSuite) TestUpdateUser(c *qt.C) { c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } err = s.Database.GetUser(ctx, &u) c.Assert(err, qt.IsNil) @@ -81,7 +81,7 @@ func (s *dbSuite) TestUpdateUser(c *qt.C) { c.Assert(err, qt.IsNil) u2 := dbmodel.Identity{ - Username: u.Username, + Name: u.Name, } err = s.Database.GetUser(ctx, &u2) c.Assert(err, qt.IsNil) @@ -108,12 +108,12 @@ func (s *dbSuite) TestGetUserCloudCredentials(c *qt.C) { c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) _, err = s.Database.GetUserCloudCredentials(ctx, &dbmodel.Identity{ - Username: "test", + Name: "test", }, "ec2") c.Check(err, qt.IsNil) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -129,7 +129,7 @@ func (s *dbSuite) TestGetUserCloudCredentials(c *qt.C) { cred1 := dbmodel.CloudCredential{ Name: "test-cred-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = s.Database.SetCloudCredential(context.Background(), &cred1) @@ -138,7 +138,7 @@ func (s *dbSuite) TestGetUserCloudCredentials(c *qt.C) { cred2 := dbmodel.CloudCredential{ Name: "test-cred-2", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = s.Database.SetCloudCredential(context.Background(), &cred2) diff --git a/internal/db/usermodeldefaults_test.go b/internal/db/usermodeldefaults_test.go index 421543407..bb88b0286 100644 --- a/internal/db/usermodeldefaults_test.go +++ b/internal/db/usermodeldefaults_test.go @@ -38,7 +38,7 @@ func TestSetUserModelDefaults(t *testing.T) { about: "defaults do not exist yet - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -48,7 +48,7 @@ func TestSetUserModelDefaults(t *testing.T) { } expectedDefaults := dbmodel.UserModelDefaults{ - Username: user.Username, + Username: user.Name, User: user, Defaults: defaults, } @@ -63,12 +63,12 @@ func TestSetUserModelDefaults(t *testing.T) { about: "defaults already exist - defaults updated", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) j.Database.SetUserModelDefaults(ctx, &dbmodel.UserModelDefaults{ - Username: user.Username, + Username: user.Name, User: user, Defaults: map[string]interface{}{ "key1": float64(17), @@ -83,7 +83,7 @@ func TestSetUserModelDefaults(t *testing.T) { } expectedDefaults := dbmodel.UserModelDefaults{ - Username: user.Username, + Username: user.Name, User: user, Defaults: defaults, } @@ -98,7 +98,7 @@ func TestSetUserModelDefaults(t *testing.T) { about: "user does not exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } defaults := map[string]interface{}{ @@ -117,7 +117,7 @@ func TestSetUserModelDefaults(t *testing.T) { about: "cannot set agent-version", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) diff --git a/internal/dbmodel/cloudcredential_test.go b/internal/dbmodel/cloudcredential_test.go index d5a0deb53..d9e4340cc 100644 --- a/internal/dbmodel/cloudcredential_test.go +++ b/internal/dbmodel/cloudcredential_test.go @@ -37,7 +37,7 @@ func TestCloudCredential(t *testing.T) { Name: "test-cloud", }, Owner: dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", }, AuthType: "empty", Label: "test label", @@ -49,7 +49,7 @@ func TestCloudCredential(t *testing.T) { result := db.Create(&cred) c.Assert(result.Error, qt.IsNil) c.Check(cred.CloudName, qt.Equals, cred.Cloud.Name) - c.Check(cred.OwnerUsername, qt.Equals, cred.Owner.Username) + c.Check(cred.OwnerUsername, qt.Equals, cred.Owner.Name) } // TestCloudCredentialsCascadeOnDelete As of database version 1.3 (see migrations), @@ -70,7 +70,7 @@ func TestCloudCredentialsCascadeOnDelete(t *testing.T) { Name: "test-credential", Cloud: cloud, Owner: dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", }, } result = db.Create(&cred) diff --git a/internal/dbmodel/controller_test.go b/internal/dbmodel/controller_test.go index fc1d8056c..efd8ff0dc 100644 --- a/internal/dbmodel/controller_test.go +++ b/internal/dbmodel/controller_test.go @@ -92,7 +92,7 @@ func TestControllerModels(t *testing.T) { c.Assert(db.Create(&m1).Error, qt.IsNil) u2 := dbmodel.Identity{ - Username: "charlie@external", + Name: "charlie@external", } c.Assert(db.Create(&u2).Error, qt.IsNil) diff --git a/internal/dbmodel/model.go b/internal/dbmodel/model.go index fb5aa23b4..4a3fa3944 100644 --- a/internal/dbmodel/model.go +++ b/internal/dbmodel/model.go @@ -104,7 +104,7 @@ func (m *Model) SetTag(t names.ModelTag) { // FromModelUpdate updates the model from the given ModelUpdate. func (m *Model) SwitchOwner(u *Identity) { - m.OwnerUsername = u.Username + m.OwnerUsername = u.Name m.Owner = *u } @@ -140,7 +140,7 @@ func (m *Model) FromJujuModelInfo(info jujuparams.ModelInfo) error { } m.CloudCredential.Name = cct.Name() m.CloudCredential.CloudName = cct.Cloud().Id() - m.CloudCredential.Owner.Username = cct.Owner().Id() + m.CloudCredential.Owner.Name = cct.Owner().Id() } if info.SLA != nil { diff --git a/internal/dbmodel/model_test.go b/internal/dbmodel/model_test.go index e4e42bef9..9b3f1632c 100644 --- a/internal/dbmodel/model_test.go +++ b/internal/dbmodel/model_test.go @@ -214,7 +214,7 @@ func TestToJujuModel(t *testing.T) { String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, Owner: u, Controller: ctl, CloudRegion: cl.Regions[0], @@ -318,7 +318,7 @@ func TestToJujuModelSummary(t *testing.T) { // that a model can be created. func initModelEnv(c *qt.C, db *gorm.DB) (dbmodel.Cloud, dbmodel.CloudCredential, dbmodel.Controller, dbmodel.Identity) { u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(db.Create(&u).Error, qt.IsNil) @@ -415,7 +415,7 @@ func TestModelFromJujuModelInfo(t *testing.T) { Name: "test-cred", CloudName: "test-cloud", Owner: dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", }, }, OwnerUsername: "bob@external", diff --git a/internal/dbmodel/user.go b/internal/dbmodel/user.go index 442fc2834..070d2b5f3 100644 --- a/internal/dbmodel/user.go +++ b/internal/dbmodel/user.go @@ -10,59 +10,59 @@ import ( "gorm.io/gorm" ) -// A Identity represents a JIMM identity, which can be a user or a service account. +// Identity represents a JIMM identity, which can be a user or a service account. type Identity struct { gorm.Model - // Username is the username for the user. This is the juju - // representation of the username (i.e. with an @external suffix). The - // username will have originated at an external identity provider in - // JAAS deployments. - Username string `gorm:"not null;uniqueIndex"` + // Name is the name of the identity. This is the user name when + // representing a Juju user (i.e. with an @external suffix), or the client + // ID for a service account. The Name will have originated at an + // external identity provider in JAAS deployments. + Name string `gorm:"not null;uniqueIndex"` - // DisplayName is the displayname of the user. + // DisplayName is the display name of the identity. DisplayName string `gorm:"not null"` - // LastLogin is the time the user last authenticated to the JIMM - // server. LastLogin will only be a valid time if the user has + // LastLogin is the time the identity last authenticated to the JIMM + // server. LastLogin will only be a valid time if the identity has // authenticated at least once. LastLogin sql.NullTime - // Disabled records whether the user has been disabled or not, disabled - // users are not allowed to authenticate. + // Disabled records whether the identity has been disabled or not, disabled + // identities are not allowed to authenticate. Disabled bool `gorm:"not null;default:FALSE"` - // CloudCredentials are the cloud credentials owned by this user. + // CloudCredentials are the cloud credentials owned by this identity. CloudCredentials []CloudCredential `gorm:"foreignKey:OwnerUsername;references:Username"` } -// Tag returns a names.Tag for the user. +// Tag returns a names.Tag for the identity. func (u Identity) Tag() names.Tag { return u.ResourceTag() } -// ResourceTag returns a tag for the user. This method +// ResourceTag returns a tag for the user. This method // is intended to be used in places where we expect to see // a concrete type names.UserTag instead of the // names.Tag interface. -func (u Identity) ResourceTag() names.UserTag { - return names.NewUserTag(u.Username) +func (i Identity) ResourceTag() names.UserTag { + return names.NewUserTag(i.Name) } -// SetTag sets the username of the user to the value from the given tag. -func (u *Identity) SetTag(t names.UserTag) { - u.Username = t.Id() +// SetTag sets the identity name of the identity to the value from the given tag. +func (i *Identity) SetTag(t names.UserTag) { + i.Name = t.Id() } -// ToJujuUserInfo converts a User into a juju UserInfo value. -func (u Identity) ToJujuUserInfo() jujuparams.UserInfo { +// ToJujuUserInfo converts an Identity into a juju UserInfo value. +func (i Identity) ToJujuUserInfo() jujuparams.UserInfo { var ui jujuparams.UserInfo - ui.Username = u.Username - ui.DisplayName = u.DisplayName + ui.Username = i.Name + ui.DisplayName = i.DisplayName ui.Access = "" //TODO(Kian) CSS-6040 Handle merging OpenFGA and Postgres information - ui.DateCreated = u.CreatedAt - if u.LastLogin.Valid { - ui.LastConnection = &u.LastLogin.Time + ui.DateCreated = i.CreatedAt + if i.LastLogin.Valid { + ui.LastConnection = &i.LastLogin.Time } return ui } diff --git a/internal/dbmodel/user_test.go b/internal/dbmodel/user_test.go index 24fdc824a..2bd80b9ce 100644 --- a/internal/dbmodel/user_test.go +++ b/internal/dbmodel/user_test.go @@ -24,7 +24,7 @@ func TestUser(t *testing.T) { c.Check(result.Error, qt.Equals, gorm.ErrRecordNotFound) u1 := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", DisplayName: "bob", } result = db.Create(&u1) @@ -46,7 +46,7 @@ func TestUser(t *testing.T) { c.Check(u3, qt.DeepEquals, u2) u4 := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", DisplayName: "bob", } result = db.Create(&u4) @@ -57,7 +57,7 @@ func TestUserTag(t *testing.T) { c := qt.New(t) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } tag := u.Tag() c.Check(tag.String(), qt.Equals, "user-bob@external") @@ -77,7 +77,7 @@ func TestUserCloudCredentials(t *testing.T) { c.Assert(result.Error, qt.IsNil) u := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } result = db.Create(&u) c.Assert(result.Error, qt.IsNil) @@ -125,7 +125,7 @@ func TestUserToJujuUserInfo(t *testing.T) { Model: gorm.Model{ CreatedAt: time.Now(), }, - Username: "alice@external", + Name: "alice@external", DisplayName: "Alice", } ui := u.ToJujuUserInfo() diff --git a/internal/jimm/access_test.go b/internal/jimm/access_test.go index a79eebce0..cc7f263e0 100644 --- a/internal/jimm/access_test.go +++ b/internal/jimm/access_test.go @@ -35,7 +35,7 @@ func (ta *testAuthenticator) Authenticate(ctx context.Context, req *jujuparams.L } return &openfga.User{ Identity: &dbmodel.Identity{ - Username: ta.username, + Name: ta.username, }, }, nil } @@ -142,11 +142,11 @@ func TestAuditLogAccess(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - adminUser := openfga.NewUser(&dbmodel.Identity{Username: "alice"}, j.OpenFGAClient) + adminUser := openfga.NewUser(&dbmodel.Identity{Name: "alice"}, j.OpenFGAClient) err = adminUser.SetControllerAccess(ctx, j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) - user := openfga.NewUser(&dbmodel.Identity{Username: "bob"}, j.OpenFGAClient) + user := openfga.NewUser(&dbmodel.Identity{Name: "bob"}, j.OpenFGAClient) // admin user can grant other users audit log access. err = j.GrantAuditLogAccess(ctx, adminUser, user.ResourceTag()) diff --git a/internal/jimm/applicationoffer.go b/internal/jimm/applicationoffer.go index f56d165dd..cd201f6ab 100644 --- a/internal/jimm/applicationoffer.go +++ b/internal/jimm/applicationoffer.go @@ -142,7 +142,7 @@ func (j *JIMM) Offer(ctx context.Context, user *openfga.User, offer AddApplicati } owner := openfga.NewUser( &dbmodel.Identity{ - Username: ownerId, + Name: ownerId, }, j.OpenFGAClient, ) @@ -156,7 +156,7 @@ func (j *JIMM) Offer(ctx context.Context, user *openfga.User, offer AddApplicati everyone := openfga.NewUser( &dbmodel.Identity{ - Username: ofganames.EveryoneUser, + Name: ofganames.EveryoneUser, }, j.OpenFGAClient, ) @@ -266,10 +266,10 @@ func (j *JIMM) listApplicationOfferUsers(ctx context.Context, offer names.Applic for _, user := range usersWithRelation { // if the user is in the users map, it must already have a higher // access level - we skip this user - if users[user.Username] != "" { + if users[user.Name] != "" { continue } - users[user.Username] = ToOfferAccessString(relation) + users[user.Name] = ToOfferAccessString(relation) } } @@ -278,7 +278,7 @@ func (j *JIMM) listApplicationOfferUsers(ctx context.Context, offer names.Applic // non-admin users should only see their own access level // and access level of "everyone@external" - meaning the access // level everybody has. - if accessLevel != string(jujuparams.OfferAdminAccess) && username != ofganames.EveryoneUser && username != user.Username { + if accessLevel != string(jujuparams.OfferAdminAccess) && username != ofganames.EveryoneUser && username != user.Name { continue } userDetails = append(userDetails, jujuparams.OfferUserDetails{ @@ -358,7 +358,7 @@ func (j *JIMM) GrantOfferAccess(ctx context.Context, user *openfga.User, offerUR const op = errors.Op("jimm.GrantOfferAccess") err := j.doApplicationOfferAdmin(ctx, user, offerURL, func(offer *dbmodel.ApplicationOffer, api API) error { - tUser := openfga.NewUser(&dbmodel.Identity{Username: ut.Id()}, j.OpenFGAClient) + tUser := openfga.NewUser(&dbmodel.Identity{Name: ut.Id()}, j.OpenFGAClient) currentRelation := tUser.GetApplicationOfferAccess(ctx, offer.ResourceTag()) currentAccessLevel := ToOfferAccessString(currentRelation) targetAccessLevel := determineAccessLevelAfterGrant(currentAccessLevel, string(access)) @@ -415,7 +415,7 @@ func (j *JIMM) RevokeOfferAccess(ctx context.Context, user *openfga.User, offerU const op = errors.Op("jimm.RevokeOfferAccess") err = j.doApplicationOfferAdmin(ctx, user, offerURL, func(offer *dbmodel.ApplicationOffer, api API) error { - tUser := openfga.NewUser(&dbmodel.Identity{Username: ut.Id()}, j.OpenFGAClient) + tUser := openfga.NewUser(&dbmodel.Identity{Name: ut.Id()}, j.OpenFGAClient) targetRelation, err := ToOfferRelation(string(access)) if err != nil { return errors.E(op, err) @@ -665,7 +665,7 @@ func (j *JIMM) applicationOfferFilters(ctx context.Context, jujuFilters ...jujup if len(f.AllowedConsumerTags) > 0 { for _, u := range f.AllowedConsumerTags { dbUser := dbmodel.Identity{ - Username: u, + Name: u, } ofgaUser := openfga.NewUser(&dbUser, j.OpenFGAClient) offerUUIDs, err := ofgaUser.ListApplicationOffers(ctx, ofganames.ConsumerRelation) @@ -702,7 +702,7 @@ func (j *JIMM) ListApplicationOffers(ctx context.Context, user *openfga.User, fi return nil, errors.E(op, "application offer filter must specify a model name") } if f.OwnerName == "" { - f.OwnerName = user.Username + f.OwnerName = user.Name } m := modelKey{ name: f.ModelName, diff --git a/internal/jimm/applicationoffer_test.go b/internal/jimm/applicationoffer_test.go index 4fe5f35f8..9a088149b 100644 --- a/internal/jimm/applicationoffer_test.go +++ b/internal/jimm/applicationoffer_test.go @@ -44,39 +44,39 @@ var initializeEnvironment = func(c *qt.C, ctx context.Context, db *db.Database, // Alice is a model admin, but not a superuser or offer admin. u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) u1 := dbmodel.Identity{ - Username: "eve@external", + Name: "eve@external", } c.Assert(db.DB.Create(&u1).Error, qt.IsNil) u2 := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(db.DB.Create(&u2).Error, qt.IsNil) u3 := dbmodel.Identity{ - Username: "fred@external", + Name: "fred@external", } c.Assert(db.DB.Create(&u3).Error, qt.IsNil) u4 := dbmodel.Identity{ - Username: "grant@external", + Name: "grant@external", } c.Assert(db.DB.Create(&u4).Error, qt.IsNil) // Jane is an offer admin, but not a superuser or model admin. u5 := dbmodel.Identity{ - Username: "jane@external", + Name: "jane@external", } c.Assert(db.DB.Create(&u5).Error, qt.IsNil) // Joe is a superuser, but not a model or offer admin. u6 := dbmodel.Identity{ - Username: "joe@external", + Name: "joe@external", } c.Assert(db.DB.Create(&u6).Error, qt.IsNil) @@ -124,7 +124,7 @@ var initializeEnvironment = func(c *qt.C, ctx context.Context, db *db.Database, cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) @@ -137,7 +137,7 @@ var initializeEnvironment = func(c *qt.C, ctx context.Context, db *db.Database, String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -569,17 +569,17 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { c.Assert(err, qt.IsNil) u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) u1 := dbmodel.Identity{ - Username: "eve@external", + Name: "eve@external", } c.Assert(db.DB.Create(&u1).Error, qt.IsNil) u2 := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(db.DB.Create(&u2).Error, qt.IsNil) @@ -614,7 +614,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) @@ -626,7 +626,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -660,7 +660,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { everyoneTag := names.NewUserTag(ofganames.EveryoneUser) uAll := dbmodel.Identity{ - Username: everyoneTag.Id(), + Name: everyoneTag.Id(), } c.Assert(db.DB.Create(&uAll).Error, qt.IsNil) // user uAll is reader of the test offer @@ -943,17 +943,17 @@ func TestGetApplicationOffer(t *testing.T) { c.Assert(err, qt.IsNil) u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) u1 := dbmodel.Identity{ - Username: "eve@external", + Name: "eve@external", } c.Assert(j.Database.DB.Create(&u1).Error, qt.IsNil) u2 := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&u2).Error, qt.IsNil) @@ -982,7 +982,7 @@ func TestGetApplicationOffer(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = j.Database.SetCloudCredential(ctx, &cred) @@ -994,7 +994,7 @@ func TestGetApplicationOffer(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1245,7 +1245,7 @@ func TestOffer(t *testing.T) { ctx := context.Background() u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1278,7 +1278,7 @@ func TestOffer(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) @@ -1290,7 +1290,7 @@ func TestOffer(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1367,7 +1367,7 @@ func TestOffer(t *testing.T) { ctx := context.Background() u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1400,7 +1400,7 @@ func TestOffer(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) @@ -1412,7 +1412,7 @@ func TestOffer(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1453,7 +1453,7 @@ func TestOffer(t *testing.T) { }, createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1488,7 +1488,7 @@ func TestOffer(t *testing.T) { ctx := context.Background() u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1521,7 +1521,7 @@ func TestOffer(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) @@ -1533,7 +1533,7 @@ func TestOffer(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1577,12 +1577,12 @@ func TestOffer(t *testing.T) { ctx := context.Background() u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) u1 := dbmodel.Identity{ - Username: "eve@external", + Name: "eve@external", } c.Assert(db.DB.Create(&u1).Error, qt.IsNil) @@ -1615,7 +1615,7 @@ func TestOffer(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) @@ -1627,7 +1627,7 @@ func TestOffer(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1670,7 +1670,7 @@ func TestOffer(t *testing.T) { ctx := context.Background() u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1703,7 +1703,7 @@ func TestOffer(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) @@ -1715,7 +1715,7 @@ func TestOffer(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1758,7 +1758,7 @@ func TestOffer(t *testing.T) { ctx := context.Background() u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1791,7 +1791,7 @@ func TestOffer(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) @@ -1803,7 +1803,7 @@ func TestOffer(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1888,7 +1888,7 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { ctx := context.Background() u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1921,7 +1921,7 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) @@ -1933,7 +1933,7 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -2568,7 +2568,7 @@ func TestFindApplicationOffers(t *testing.T) { details := test.expectedOffer.ToJujuApplicationOfferDetails() if accessLevel != string(jujuparams.OfferAdminAccess) { details.Users = []jujuparams.OfferUserDetails{{ - UserName: user.Username, + UserName: user.Name, Access: accessLevel, }} } else { diff --git a/internal/jimm/cloud.go b/internal/jimm/cloud.go index 51a7be859..0a8b0d598 100644 --- a/internal/jimm/cloud.go +++ b/internal/jimm/cloud.go @@ -27,7 +27,7 @@ func (j *JIMM) GetUserCloudAccess(ctx context.Context, user *openfga.User, cloud everyoneTag := names.NewUserTag(ofganames.EveryoneUser) everyone := openfga.NewUser( &dbmodel.Identity{ - Username: everyoneTag.Id(), + Name: everyoneTag.Id(), }, j.OpenFGAClient, ) @@ -100,7 +100,7 @@ func (j *JIMM) ForEachUserCloud(ctx context.Context, user *openfga.User, f func( // Also include "public" clouds everyoneDB := dbmodel.Identity{ - Username: ofganames.EveryoneUser, + Name: ofganames.EveryoneUser, } everyone := openfga.NewUser(&everyoneDB, j.OpenFGAClient) @@ -396,7 +396,7 @@ func (j *JIMM) AddHostedCloud(ctx context.Context, user *openfga.User, tag names zapctx.Error( ctx, "failed to add user as cloud admin", - zap.String("user", user.Username), + zap.String("user", user.Name), zap.String("cloud", dbCloud.ResourceTag().Id()), zap.Error(err), ) diff --git a/internal/jimm/cloud_test.go b/internal/jimm/cloud_test.go index e12c696db..426ae6794 100644 --- a/internal/jimm/cloud_test.go +++ b/internal/jimm/cloud_test.go @@ -42,22 +42,22 @@ func TestGetCloud(t *testing.T) { alice := openfga.NewUser( &dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", }, client, ) bob := openfga.NewUser( &dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", }, client, ) - charlie := openfga.NewUser(&dbmodel.Identity{Username: "charlie@external"}, client) + charlie := openfga.NewUser(&dbmodel.Identity{Name: "charlie@external"}, client) // daphne is a jimm administrator daphne := openfga.NewUser( &dbmodel.Identity{ - Username: "daphne@external", + Name: "daphne@external", }, client, ) @@ -70,7 +70,7 @@ func TestGetCloud(t *testing.T) { everyone := openfga.NewUser( &dbmodel.Identity{ - Username: ofganames.EveryoneUser, + Name: ofganames.EveryoneUser, }, client, ) @@ -169,26 +169,26 @@ func TestForEachCloud(t *testing.T) { c.Assert(err, qt.IsNil) alice := openfga.NewUser( - &dbmodel.Identity{Username: "alice@external"}, + &dbmodel.Identity{Name: "alice@external"}, client, ) bob := openfga.NewUser( - &dbmodel.Identity{Username: "bob@external"}, + &dbmodel.Identity{Name: "bob@external"}, client, ) charlie := openfga.NewUser( - &dbmodel.Identity{Username: "charlie@external"}, + &dbmodel.Identity{Name: "charlie@external"}, client, ) daphne := openfga.NewUser( - &dbmodel.Identity{Username: "daphne@external"}, + &dbmodel.Identity{Name: "daphne@external"}, client, ) daphne.JimmAdmin = true everyone := openfga.NewUser( &dbmodel.Identity{ - Username: ofganames.EveryoneUser, + Name: ofganames.EveryoneUser, }, client, ) diff --git a/internal/jimm/cloudcredential.go b/internal/jimm/cloudcredential.go index e8cb78091..164102893 100644 --- a/internal/jimm/cloudcredential.go +++ b/internal/jimm/cloudcredential.go @@ -30,7 +30,7 @@ import ( func (j *JIMM) GetCloudCredential(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) { const op = errors.Op("jimm.GetCloudCredential") - if !user.JimmAdmin && user.Username != tag.Owner().Id() { + if !user.JimmAdmin && user.Name != tag.Owner().Id() { return nil, errors.E(op, errors.CodeUnauthorized, "unauthorized") } @@ -51,7 +51,7 @@ func (j *JIMM) GetCloudCredential(ctx context.Context, user *openfga.User, tag n func (j *JIMM) RevokeCloudCredential(ctx context.Context, user *dbmodel.Identity, tag names.CloudCredentialTag, force bool) error { const op = errors.Op("jimm.RevokeCloudCredential") - if user.Username != tag.Owner().Id() { + if user.Name != tag.Owner().Id() { return errors.E(op, errors.CodeUnauthorized, "unauthorized") } @@ -294,7 +294,7 @@ func (j *JIMM) ForEachUserCloudCredential(ctx context.Context, u *dbmodel.Identi errStop := errors.E("stop") var iterErr error - err := j.Database.ForEachCloudCredential(ctx, u.Username, cloud, func(cred *dbmodel.CloudCredential) error { + err := j.Database.ForEachCloudCredential(ctx, u.Name, cloud, func(cred *dbmodel.CloudCredential) error { cred.Attributes = nil iterErr = f(cred) if iterErr != nil { @@ -321,11 +321,11 @@ func (j *JIMM) GetCloudCredentialAttributes(ctx context.Context, user *openfga.U if hidden { // Controller superusers cannot read hidden credential attributes. - if user.Username != cred.OwnerUsername { + if user.Name != cred.OwnerUsername { return nil, nil, errors.E(op, errors.CodeUnauthorized, "unauthorized") } } else { - if !user.JimmAdmin && user.Username != cred.OwnerUsername { + if !user.JimmAdmin && user.Name != cred.OwnerUsername { return nil, nil, errors.E(op, errors.CodeUnauthorized, "unauthorized") } } diff --git a/internal/jimm/cloudcredential_test.go b/internal/jimm/cloudcredential_test.go index 038165f38..1bfd10525 100644 --- a/internal/jimm/cloudcredential_test.go +++ b/internal/jimm/cloudcredential_test.go @@ -45,7 +45,7 @@ func TestUpdateCloudCredential(t *testing.T) { jimmAdmin: true, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -96,7 +96,7 @@ func TestUpdateCloudCredential(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) @@ -109,7 +109,7 @@ func TestUpdateCloudCredential(t *testing.T) { mi, err := j.AddModel(context.Background(), user, &jimm.ModelCreateArgs{ Name: "test-model", - Owner: names.NewUserTag(u.Username), + Owner: names.NewUserTag(u.Name), Cloud: names.NewCloudTag(cloud.Name), CloudRegion: "test-region-1", CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), @@ -159,7 +159,7 @@ func TestUpdateCloudCredential(t *testing.T) { updateCredentialErrors: []error{nil, errors.E("test error")}, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -211,7 +211,7 @@ func TestUpdateCloudCredential(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) @@ -224,7 +224,7 @@ func TestUpdateCloudCredential(t *testing.T) { _, err = j.AddModel(context.Background(), user, &jimm.ModelCreateArgs{ Name: "test-model", - Owner: names.NewUserTag(u.Username), + Owner: names.NewUserTag(u.Name), Cloud: names.NewCloudTag(cloud.Name), CloudRegion: "test-region-1", CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), @@ -250,7 +250,7 @@ func TestUpdateCloudCredential(t *testing.T) { updateCredentialErrors: []error{nil}, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -302,7 +302,7 @@ func TestUpdateCloudCredential(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) @@ -310,7 +310,7 @@ func TestUpdateCloudCredential(t *testing.T) { _, err = j.AddModel(context.Background(), user, &jimm.ModelCreateArgs{ Name: "test-model", - Owner: names.NewUserTag(u.Username), + Owner: names.NewUserTag(u.Name), Cloud: names.NewCloudTag(cloud.Name), CloudRegion: "test-region-1", CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), @@ -334,7 +334,7 @@ func TestUpdateCloudCredential(t *testing.T) { jimmAdmin: true, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -345,7 +345,7 @@ func TestUpdateCloudCredential(t *testing.T) { c.Assert(err, qt.IsNil) eve := dbmodel.Identity{ - Username: "eve@external", + Name: "eve@external", } c.Assert(j.Database.DB.Create(&eve).Error, qt.IsNil) @@ -396,7 +396,7 @@ func TestUpdateCloudCredential(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: eve.Username, + OwnerUsername: eve.Name, AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) @@ -443,7 +443,7 @@ func TestUpdateCloudCredential(t *testing.T) { Name: cloud.Name, Type: cloud.Type, }, - OwnerUsername: eve.Username, + OwnerUsername: eve.Name, Attributes: map[string]string{ "key1": "value1", "key2": "value2", @@ -458,7 +458,7 @@ func TestUpdateCloudCredential(t *testing.T) { jimmAdmin: true, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -510,7 +510,7 @@ func TestUpdateCloudCredential(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) @@ -523,7 +523,7 @@ func TestUpdateCloudCredential(t *testing.T) { mi, err := j.AddModel(context.Background(), user, &jimm.ModelCreateArgs{ Name: "test-model", - Owner: names.NewUserTag(u.Username), + Owner: names.NewUserTag(u.Name), Cloud: names.NewCloudTag(cloud.Name), CloudRegion: "test-region-1", CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), @@ -571,7 +571,7 @@ func TestUpdateCloudCredential(t *testing.T) { jimmAdmin: true, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -623,7 +623,7 @@ func TestUpdateCloudCredential(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) @@ -635,7 +635,7 @@ func TestUpdateCloudCredential(t *testing.T) { } mi, err := j.AddModel(context.Background(), user, &jimm.ModelCreateArgs{ Name: "test-model", - Owner: names.NewUserTag(u.Username), + Owner: names.NewUserTag(u.Name), Cloud: names.NewCloudTag(cloud.Name), CloudRegion: "test-region-1", CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), @@ -877,7 +877,7 @@ func TestRevokeCloudCredential(t *testing.T) { about: "credential revoked", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -929,7 +929,7 @@ func TestRevokeCloudCredential(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) @@ -951,7 +951,7 @@ func TestRevokeCloudCredential(t *testing.T) { }}, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1003,7 +1003,7 @@ func TestRevokeCloudCredential(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) @@ -1021,7 +1021,7 @@ func TestRevokeCloudCredential(t *testing.T) { about: "credential still used by a model", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1073,7 +1073,7 @@ func TestRevokeCloudCredential(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) @@ -1081,7 +1081,7 @@ func TestRevokeCloudCredential(t *testing.T) { _, err = j.AddModel(context.Background(), alice, &jimm.ModelCreateArgs{ Name: "test-model", - Owner: names.NewUserTag(u.Username), + Owner: names.NewUserTag(u.Name), Cloud: names.NewCloudTag(cloud.Name), CloudRegion: "test-region-1", CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), @@ -1096,7 +1096,7 @@ func TestRevokeCloudCredential(t *testing.T) { about: "user not owner of credentials - unauthorizer error", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1114,7 +1114,7 @@ func TestRevokeCloudCredential(t *testing.T) { revokeCredentialErrors: []error{errors.E("test error")}, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1166,7 +1166,7 @@ func TestRevokeCloudCredential(t *testing.T) { cred := dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) @@ -1286,7 +1286,7 @@ func TestGetCloudCredential(t *testing.T) { about: "all ok", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1342,7 +1342,7 @@ func TestGetCloudCredential(t *testing.T) { Name: cloud.Name, Type: cloud.Type, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) @@ -1356,7 +1356,7 @@ func TestGetCloudCredential(t *testing.T) { about: "credential not found", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) diff --git a/internal/jimm/clouddefaults.go b/internal/jimm/clouddefaults.go index 64a06234c..b654a6555 100644 --- a/internal/jimm/clouddefaults.go +++ b/internal/jimm/clouddefaults.go @@ -54,7 +54,7 @@ func (j *JIMM) SetModelDefaults(ctx context.Context, user *dbmodel.Identity, clo } } err = j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, CloudID: cloud.ID, Region: region, Defaults: configs, @@ -70,7 +70,7 @@ func (j *JIMM) UnsetModelDefaults(ctx context.Context, user *dbmodel.Identity, c const op = errors.Op("jimm.UnsetModelDefaults") defaults := dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, Cloud: dbmodel.Cloud{ Name: cloudTag.Id(), }, diff --git a/internal/jimm/clouddefaults_test.go b/internal/jimm/clouddefaults_test.go index 6a43c0df9..02b996899 100644 --- a/internal/jimm/clouddefaults_test.go +++ b/internal/jimm/clouddefaults_test.go @@ -42,7 +42,7 @@ func TestSetCloudDefaults(t *testing.T) { about: "defaults do not exist yet - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -62,7 +62,7 @@ func TestSetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, User: user, CloudID: cloud.ID, Cloud: cloud, @@ -82,7 +82,7 @@ func TestSetCloudDefaults(t *testing.T) { about: "set defaults without region - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -102,7 +102,7 @@ func TestSetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, User: user, CloudID: cloud.ID, Cloud: cloud, @@ -121,7 +121,7 @@ func TestSetCloudDefaults(t *testing.T) { about: "defaults already exist - defaults updated", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -135,7 +135,7 @@ func TestSetCloudDefaults(t *testing.T) { c.Assert(j.Database.DB.Create(&cloud).Error, qt.IsNil) j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, User: user, CloudID: cloud.ID, Cloud: cloud, @@ -154,7 +154,7 @@ func TestSetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, User: user, CloudID: cloud.ID, Cloud: cloud, @@ -174,7 +174,7 @@ func TestSetCloudDefaults(t *testing.T) { about: "cloudregion does not exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -204,7 +204,7 @@ func TestSetCloudDefaults(t *testing.T) { about: "cannot set agent-version", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -287,7 +287,7 @@ func TestUnsetCloudDefaults(t *testing.T) { about: "all ok - keys removed from the defaults map", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -301,7 +301,7 @@ func TestUnsetCloudDefaults(t *testing.T) { c.Assert(j.Database.DB.Create(&cloud).Error, qt.IsNil) err := j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, CloudID: cloud.ID, Region: cloud.Regions[0].Name, Defaults: map[string]interface{}{ @@ -320,7 +320,7 @@ func TestUnsetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, User: user, CloudID: cloud.ID, Cloud: cloud, @@ -342,7 +342,7 @@ func TestUnsetCloudDefaults(t *testing.T) { about: "unset without region - keys removed from the defaults map", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -356,7 +356,7 @@ func TestUnsetCloudDefaults(t *testing.T) { c.Assert(j.Database.DB.Create(&cloud).Error, qt.IsNil) err := j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, CloudID: cloud.ID, Defaults: map[string]interface{}{ "key1": float64(17), @@ -374,7 +374,7 @@ func TestUnsetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, User: user, CloudID: cloud.ID, Cloud: cloud, @@ -396,7 +396,7 @@ func TestUnsetCloudDefaults(t *testing.T) { about: "cloudregiondefaults not found", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -472,12 +472,12 @@ func TestModelDefaultsForCloud(t *testing.T) { c.Assert(err, qt.Equals, nil) user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) user1 := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } c.Assert(j.Database.DB.Create(&user1).Error, qt.IsNil) @@ -502,7 +502,7 @@ func TestModelDefaultsForCloud(t *testing.T) { c.Assert(j.Database.DB.Create(&cloud2).Error, qt.IsNil) err = j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, CloudID: cloud1.ID, Region: cloud1.Regions[0].Name, Defaults: map[string]interface{}{ @@ -514,7 +514,7 @@ func TestModelDefaultsForCloud(t *testing.T) { c.Assert(err, qt.Equals, nil) err = j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, CloudID: cloud1.ID, Region: cloud1.Regions[1].Name, Defaults: map[string]interface{}{ @@ -525,7 +525,7 @@ func TestModelDefaultsForCloud(t *testing.T) { c.Assert(err, qt.Equals, nil) err = j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, CloudID: cloud2.ID, Region: cloud2.Regions[0].Name, Defaults: map[string]interface{}{ @@ -537,7 +537,7 @@ func TestModelDefaultsForCloud(t *testing.T) { c.Assert(err, qt.Equals, nil) err = j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, CloudID: cloud2.ID, Region: "", Defaults: map[string]interface{}{ diff --git a/internal/jimm/controller.go b/internal/jimm/controller.go index d5cc10654..71fb24291 100644 --- a/internal/jimm/controller.go +++ b/internal/jimm/controller.go @@ -170,7 +170,7 @@ func (j *JIMM) AddController(ctx context.Context, user *openfga.User, ctl *dbmod everyoneTag := names.NewUserTag(ofganames.EveryoneUser) everyone := openfga.NewUser( &dbmodel.Identity{ - Username: everyoneTag.Id(), + Name: everyoneTag.Id(), }, j.OpenFGAClient, ) @@ -287,7 +287,7 @@ func (j *JIMM) GetJimmControllerAccess(ctx context.Context, user *openfga.User, // for him/her-self then we return that - either the user // is a JIMM admin (aka "superuser"), or they have a "login" // access level. - if user.Username == tag.Id() { + if user.Name == tag.Id() { if user.JimmAdmin { return "superuser", nil } @@ -393,7 +393,7 @@ func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerNa zapctx.Error( ctx, "failed to set model admin", - zap.String("owner", ownerUser.Username), + zap.String("owner", ownerUser.Name), zap.String("model", modelTag.String()), zap.Error(err), ) @@ -417,7 +417,7 @@ func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerNa return errors.E(op, err) } if len(allCredentials) == 0 { - return errors.E(op, errors.CodeNotFound, fmt.Sprintf("Failed to find cloud credential for user %s on cloud %s", ownerUser.Username, cloudTag.Id())) + return errors.E(op, errors.CodeNotFound, fmt.Sprintf("Failed to find cloud credential for user %s on cloud %s", ownerUser.Name, cloudTag.Id())) } cloudCredential := allCredentials[0] diff --git a/internal/jimm/controller_test.go b/internal/jimm/controller_test.go index af1f1bdc8..0680f80fd 100644 --- a/internal/jimm/controller_test.go +++ b/internal/jimm/controller_test.go @@ -148,7 +148,7 @@ func TestAddController(t *testing.T) { c.Assert(err, qt.IsNil) u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } alice := openfga.NewUser(&u, client) @@ -316,7 +316,7 @@ func TestAddControllerWithVault(t *testing.T) { c.Assert(err, qt.IsNil) u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } alice := openfga.NewUser(&u, ofgaClient) alice.JimmAdmin = true @@ -603,7 +603,7 @@ func TestImportModel(t *testing.T) { Valid: true, }, Owner: dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", DisplayName: "Alice", }, Controller: dbmodel.Controller{ @@ -691,7 +691,7 @@ func TestImportModel(t *testing.T) { Valid: true, }, Owner: dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", DisplayName: "Alice", }, Controller: dbmodel.Controller{ @@ -1437,7 +1437,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", }, client, ) @@ -1462,7 +1462,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", }, client, ) @@ -1482,7 +1482,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", }, client, ) @@ -1503,7 +1503,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", }, client, ) @@ -1522,7 +1522,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", }, client, ) @@ -1541,7 +1541,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", }, client, ) @@ -1560,7 +1560,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", }, client, ) @@ -1579,7 +1579,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", }, client, ) diff --git a/internal/jimm/jimm_test.go b/internal/jimm/jimm_test.go index bfcc4ccc0..8ef5e5364 100644 --- a/internal/jimm/jimm_test.go +++ b/internal/jimm/jimm_test.go @@ -45,15 +45,15 @@ func TestFindAuditEvents(t *testing.T) { err = j.Database.Migrate(ctx, true) c.Assert(err, qt.Equals, nil) - admin := openfga.NewUser(&dbmodel.Identity{Username: "alice@external"}, client) + admin := openfga.NewUser(&dbmodel.Identity{Name: "alice@external"}, client) err = admin.SetControllerAccess(ctx, j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) - privileged := openfga.NewUser(&dbmodel.Identity{Username: "bob@external"}, client) + privileged := openfga.NewUser(&dbmodel.Identity{Name: "bob@external"}, client) err = privileged.SetControllerAccess(ctx, j.ResourceTag(), ofganames.AuditLogViewerRelation) c.Assert(err, qt.IsNil) - unprivileged := openfga.NewUser(&dbmodel.Identity{Username: "eve@external"}, client) + unprivileged := openfga.NewUser(&dbmodel.Identity{Name: "eve@external"}, client) events := []dbmodel.AuditLogEntry{{ Time: now, diff --git a/internal/jimm/model.go b/internal/jimm/model.go index af2debc7e..46d9aaaf2 100644 --- a/internal/jimm/model.go +++ b/internal/jimm/model.go @@ -315,7 +315,7 @@ func (b *modelBuilder) CreateDatabaseModel() *modelBuilder { err := b.jimm.Database.AddModel(b.ctx, b.model) if err != nil { if errors.ErrorCode(err) == errors.CodeAlreadyExists { - b.err = errors.E(err, fmt.Sprintf("model %s/%s already exists", b.owner.Username, b.name)) + b.err = errors.E(err, fmt.Sprintf("model %s/%s already exists", b.owner.Name, b.name)) return b } else { zapctx.Error(b.ctx, "failed to store model information", zaputil.Error(err)) @@ -339,7 +339,7 @@ func (b *modelBuilder) Cleanup() { // context expiration ctx := context.Background() if derr := b.jimm.Database.DeleteModel(ctx, b.model); derr != nil { - zapctx.Error(ctx, "failed to delete model", zap.String("model", b.model.Name), zap.String("owner", b.model.Owner.Username), zaputil.Error(derr)) + zapctx.Error(ctx, "failed to delete model", zap.String("model", b.model.Name), zap.String("owner", b.model.Owner.Name), zaputil.Error(derr)) } } @@ -520,7 +520,7 @@ func (j *JIMM) AddModel(ctx context.Context, user *openfga.User, args *ModelCrea const op = errors.Op("jimm.AddModel") owner := &dbmodel.Identity{ - Username: args.Owner.Id(), + Name: args.Owner.Id(), } err = j.Database.GetUser(ctx, owner) if err != nil { @@ -528,7 +528,7 @@ func (j *JIMM) AddModel(ctx context.Context, user *openfga.User, args *ModelCrea } // Only JIMM admins are able to add models on behalf of other users. - if owner.Username != user.Username && !user.JimmAdmin { + if owner.Name != user.Name && !user.JimmAdmin { return nil, errors.E(op, errors.CodeUnauthorized, "unauthorized") } @@ -549,7 +549,7 @@ func (j *JIMM) AddModel(ctx context.Context, user *openfga.User, args *ModelCrea // fetch cloud defaults if args.Cloud != (names.CloudTag{}) { cloudDefaults := dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, Cloud: dbmodel.Cloud{ Name: args.Cloud.Id(), }, @@ -585,7 +585,7 @@ func (j *JIMM) AddModel(ctx context.Context, user *openfga.User, args *ModelCrea // fetch cloud region defaults if args.Cloud != (names.CloudTag{}) && builder.cloudRegion != "" { cloudRegionDefaults := dbmodel.CloudDefaults{ - Username: user.Username, + Username: user.Name, Cloud: dbmodel.Cloud{ Name: args.Cloud.Id(), }, @@ -706,8 +706,8 @@ func (j *JIMM) ModelInfo(ctx context.Context, user *openfga.User, mt names.Model // Since we are checking user relations in decreasing level of // access privilege, we want to make sure the user has not // already been recorded with a higher access level. - if _, ok := userAccess[u.Username]; !ok { - userAccess[u.Username] = ToModelAccessString(relation) + if _, ok := userAccess[u.Name]; !ok { + userAccess[u.Name] = ToModelAccessString(relation) } } } @@ -721,7 +721,7 @@ func (j *JIMM) ModelInfo(ctx context.Context, user *openfga.User, mt names.Model if !strings.Contains(username, "@") { continue } - if modelAccess == "admin" || username == user.Username || username == ofganames.EveryoneUser { + if modelAccess == "admin" || username == user.Name || username == ofganames.EveryoneUser { users = append(users, jujuparams.ModelUserInfo{ UserName: username, Access: jujuparams.UserAccessPermission(access), diff --git a/internal/jimm/model_test.go b/internal/jimm/model_test.go index fedc494e3..79ee6a319 100644 --- a/internal/jimm/model_test.go +++ b/internal/jimm/model_test.go @@ -238,7 +238,7 @@ users: Valid: true, }, Owner: dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", }, Controller: dbmodel.Controller{ Name: "controller-2", @@ -352,7 +352,7 @@ users: Valid: true, }, Owner: dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", }, Controller: dbmodel.Controller{ Name: "controller-2", @@ -445,7 +445,7 @@ users: Valid: true, }, Owner: dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", }, Controller: dbmodel.Controller{ Name: "controller-2", @@ -545,7 +545,7 @@ users: Valid: true, }, Owner: dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", }, Controller: dbmodel.Controller{ Name: "controller-2", @@ -946,7 +946,7 @@ func TestAddModel(t *testing.T) { context.Background(), openfga.NewUser( &dbmodel.Identity{ - Username: ownerId, + Name: ownerId, }, client, ), @@ -1297,7 +1297,7 @@ func TestModelInfo(t *testing.T) { env.PopulateDBAndPermissions(c, j.ResourceTag(), j.Database, client) dbUser := &dbmodel.Identity{ - Username: test.username, + Name: test.username, } user := openfga.NewUser(dbUser, client) @@ -3354,7 +3354,7 @@ var updateModelCredentialTests = []struct { Valid: true, }, Owner: dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", }, Controller: dbmodel.Controller{ Name: "controller-1", diff --git a/internal/jimm/user.go b/internal/jimm/user.go index 5fd25287d..44c2dc3b4 100644 --- a/internal/jimm/user.go +++ b/internal/jimm/user.go @@ -33,7 +33,7 @@ func (j *JIMM) Authenticate(ctx context.Context, req *jujuparams.LoginRequest) ( err = j.Database.Transaction(func(tx *db.Database) error { pu := dbmodel.Identity{ - Username: u.Username, + Name: u.Name, } if err := tx.GetUser(ctx, &pu); err != nil { return err diff --git a/internal/jimm/user_test.go b/internal/jimm/user_test.go index e10d770c5..98752d35e 100644 --- a/internal/jimm/user_test.go +++ b/internal/jimm/user_test.go @@ -53,14 +53,14 @@ func TestAuthenticate(t *testing.T) { auth.User = openfga.NewUser( &dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", DisplayName: "Bob", }, client, ) u, err := j.Authenticate(ctx, nil) c.Assert(err, qt.IsNil) - c.Check(u.Username, qt.Equals, "bob@external") + c.Check(u.Name, qt.Equals, "bob@external") c.Check(u.JimmAdmin, qt.IsFalse) err = auth.User.SetControllerAccess( @@ -72,18 +72,18 @@ func TestAuthenticate(t *testing.T) { u, err = j.Authenticate(ctx, nil) c.Assert(err, qt.IsNil) - c.Check(u.Username, qt.Equals, "bob@external") + c.Check(u.Name, qt.Equals, "bob@external") c.Check(u.JimmAdmin, qt.IsTrue) u2 := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } err = j.Database.GetUser(ctx, &u2) c.Assert(err, qt.IsNil) c.Check(u2, qt.DeepEquals, dbmodel.Identity{ Model: u.Model, - Username: "bob@external", + Name: "bob@external", DisplayName: "Bob", LastLogin: sql.NullTime{ Time: now, diff --git a/internal/jimm/usermodeldefaults.go b/internal/jimm/usermodeldefaults.go index 0cbceef04..2975f622f 100644 --- a/internal/jimm/usermodeldefaults.go +++ b/internal/jimm/usermodeldefaults.go @@ -20,7 +20,7 @@ func (j *JIMM) SetUserModelDefaults(ctx context.Context, user *dbmodel.Identity, } err := j.Database.SetUserModelDefaults(ctx, &dbmodel.UserModelDefaults{ - Username: user.Username, + Username: user.Name, Defaults: configs, }) if err != nil { @@ -34,7 +34,7 @@ func (j *JIMM) UserModelDefaults(ctx context.Context, user *dbmodel.Identity) (m const op = errors.Op("jimm.UserModelDefaults") defaults := dbmodel.UserModelDefaults{ - Username: user.Username, + Username: user.Name, } err := j.Database.UserModelDefaults(ctx, &defaults) if err != nil { diff --git a/internal/jimm/usermodeldefaults_test.go b/internal/jimm/usermodeldefaults_test.go index 3caafd51a..d0a0746ae 100644 --- a/internal/jimm/usermodeldefaults_test.go +++ b/internal/jimm/usermodeldefaults_test.go @@ -38,7 +38,7 @@ func TestSetUserModelDefaults(t *testing.T) { about: "defaults do not exist yet - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -48,7 +48,7 @@ func TestSetUserModelDefaults(t *testing.T) { } expectedDefaults := dbmodel.UserModelDefaults{ - Username: user.Username, + Username: user.Name, User: user, Defaults: defaults, } @@ -63,12 +63,12 @@ func TestSetUserModelDefaults(t *testing.T) { about: "defaults already exist - defaults updated", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) j.Database.SetUserModelDefaults(ctx, &dbmodel.UserModelDefaults{ - Username: user.Username, + Username: user.Name, User: user, Defaults: map[string]interface{}{ "key1": float64(17), @@ -83,7 +83,7 @@ func TestSetUserModelDefaults(t *testing.T) { } expectedDefaults := dbmodel.UserModelDefaults{ - Username: user.Username, + Username: user.Name, User: user, Defaults: defaults, } @@ -98,7 +98,7 @@ func TestSetUserModelDefaults(t *testing.T) { about: "cannot set agent-version", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -164,7 +164,7 @@ func TestUserModelDefaults(t *testing.T) { about: "defaults do not exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -177,12 +177,12 @@ func TestUserModelDefaults(t *testing.T) { about: "defaults exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) j.Database.SetUserModelDefaults(ctx, &dbmodel.UserModelDefaults{ - Username: user.Username, + Username: user.Name, User: user, Defaults: map[string]interface{}{ "key1": float64(42), diff --git a/internal/jimmtest/env.go b/internal/jimmtest/env.go index 3d98ac960..f71ab55e8 100644 --- a/internal/jimmtest/env.go +++ b/internal/jimmtest/env.go @@ -167,7 +167,7 @@ func (m Model) addModelRelations(c *qt.C, jimmTag names.ControllerTag, db db.Dat case "read": relation = ofganames.ReaderRelation default: - c.Fatalf("unknown model access: %s %s", dbUser.Username, u.Access) + c.Fatalf("unknown model access: %s %s", dbUser.Name, u.Access) } user := openfga.NewUser(&dbUser, client) err := user.SetModelAccess(context.Background(), m.dbo.ResourceTag(), relation) @@ -182,7 +182,7 @@ func (m Model) addModelRelations(c *qt.C, jimmTag names.ControllerTag, db db.Dat func (ctl Controller) addControllerRelations(c *qt.C, jimmTag names.ControllerTag, db db.Database, client *openfga.OFGAClient) { if ctl.dbo.AdminUser != "" { user := openfga.NewUser(&dbmodel.Identity{ - Username: ctl.dbo.AdminUser, + Name: ctl.dbo.AdminUser, }, client) err := user.SetControllerAccess(context.Background(), ctl.dbo.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) @@ -355,7 +355,7 @@ func (cc *CloudCredential) DBObject(c *qt.C, db db.Database) dbmodel.CloudCreden cc.dbo.Cloud = cc.env.Cloud(cc.Cloud).DBObject(c, db) cc.dbo.CloudName = cc.dbo.Cloud.Name cc.dbo.Owner = cc.env.User(cc.Owner).DBObject(c, db) - cc.dbo.OwnerUsername = cc.dbo.Owner.Username + cc.dbo.OwnerUsername = cc.dbo.Owner.Name cc.dbo.AuthType = cc.AuthType cc.dbo.Attributes = cc.Attributes @@ -491,7 +491,7 @@ func (u *User) DBObject(c *qt.C, db db.Database) dbmodel.Identity { if u.dbo.ID != 0 { return u.dbo } - u.dbo.Username = u.Username + u.dbo.Name = u.Username u.dbo.DisplayName = u.DisplayName err := db.UpdateUser(context.Background(), &u.dbo) diff --git a/internal/jimmtest/suite.go b/internal/jimmtest/suite.go index 694315d11..6cf3287d1 100644 --- a/internal/jimmtest/suite.go +++ b/internal/jimmtest/suite.go @@ -89,7 +89,7 @@ func (s *JIMMSuite) SetUpTest(c *gc.C) { err = s.JIMM.Database.Migrate(ctx, false) c.Assert(err, gc.Equals, nil) s.AdminUser = &dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", LastLogin: db.Now(), } err = s.JIMM.Database.GetUser(ctx, s.AdminUser) @@ -176,7 +176,7 @@ func (s *JIMMSuite) AddController(c *gc.C, name string, info *api.Info) { func (s *JIMMSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, cred jujuparams.CloudCredential) { ctx := context.Background() u := dbmodel.Identity{ - Username: tag.Owner().Id(), + Name: tag.Owner().Id(), } user := openfga.NewUser(&u, s.JIMM.OpenFGAClient) err := s.JIMM.Database.GetUser(ctx, &u) @@ -192,7 +192,7 @@ func (s *JIMMSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, func (s *JIMMSuite) AddModel(c *gc.C, owner names.UserTag, name string, cloud names.CloudTag, region string, cred names.CloudCredentialTag) names.ModelTag { ctx := context.Background() u := dbmodel.Identity{ - Username: owner.Id(), + Name: owner.Id(), } err := s.JIMM.Database.GetUser(ctx, &u) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/access_control.go b/internal/jujuapi/access_control.go index d7757a038..aecccdcdc 100644 --- a/internal/jujuapi/access_control.go +++ b/internal/jujuapi/access_control.go @@ -363,7 +363,7 @@ func (r *controllerRoot) CheckRelation(ctx context.Context, req apiparams.CheckR return checkResp, errors.E(op, errors.CodeFailedToParseTupleKey, err) } - userCheckingSelf := parsedTuple.Object.Kind == openfga.UserType && parsedTuple.Object.ID == r.user.Username + userCheckingSelf := parsedTuple.Object.Kind == openfga.UserType && parsedTuple.Object.ID == r.user.Name // Admins can check any relation, non-admins can only check their own. if !(r.user.JimmAdmin || userCheckingSelf) { return checkResp, errors.E(op, errors.CodeUnauthorized, "unauthorized") diff --git a/internal/jujuapi/access_control_test.go b/internal/jujuapi/access_control_test.go index 539eaf94b..e80ffae4c 100644 --- a/internal/jujuapi/access_control_test.go +++ b/internal/jujuapi/access_control_test.go @@ -272,9 +272,9 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { tagTests := []tagTest{ // Test user -> controller by name { - input: tuple{"user-" + user.Username, "administrator", "controller-" + controller.Name}, + input: tuple{"user-" + user.Name, "administrator", "controller-" + controller.Name}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "administrator", "controller:"+controller.UUID, ), @@ -283,9 +283,9 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { }, // Test user -> controller jimm { - input: tuple{"user-" + user.Username, "administrator", "controller-jimm"}, + input: tuple{"user-" + user.Name, "administrator", "controller-jimm"}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "administrator", "controller:"+s.JIMM.UUID, ), @@ -294,9 +294,9 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { }, // Test user -> controller by UUID { - input: tuple{"user-" + user.Username, "administrator", "controller-" + controller.UUID}, + input: tuple{"user-" + user.Name, "administrator", "controller-" + controller.UUID}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "administrator", "controller:"+controller.UUID, ), @@ -305,9 +305,9 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { }, //Test user -> group { - input: tuple{"user-" + user.Username, "member", "group-" + group.Name}, + input: tuple{"user-" + user.Name, "member", "group-" + group.Name}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "member", "group:"+stringGroupID(group.ID), ), @@ -338,9 +338,9 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { }, //Test user -> model by name { - input: tuple{"user-" + user.Username, "writer", "model-" + controller.Name + ":" + user.Username + "/" + model.Name}, + input: tuple{"user-" + user.Name, "writer", "model-" + controller.Name + ":" + user.Name + "/" + model.Name}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "writer", "model:"+model.UUID.String, ), @@ -349,9 +349,9 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { }, // Test user -> model by UUID { - input: tuple{"user-" + user.Username, "writer", "model-" + model.UUID.String}, + input: tuple{"user-" + user.Name, "writer", "model-" + model.UUID.String}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "writer", "model:"+model.UUID.String, ), @@ -360,9 +360,9 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { }, // Test user -> applicationoffer by name { - input: tuple{"user-" + user.Username, "consumer", "applicationoffer-" + controller.Name + ":" + user.Username + "/" + model.Name + "." + offer.Name}, + input: tuple{"user-" + user.Name, "consumer", "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "consumer", "applicationoffer:"+offer.UUID, ), @@ -371,9 +371,9 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { }, // Test user -> applicationoffer by UUID { - input: tuple{"user-" + user.Username, "consumer", "applicationoffer-" + offer.UUID}, + input: tuple{"user-" + user.Name, "consumer", "applicationoffer-" + offer.UUID}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "consumer", "applicationoffer:"+offer.UUID, ), @@ -404,7 +404,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { }, // Test group -> model by name { - input: tuple{"group-" + group.Name + "#member", "writer", "model-" + controller.Name + ":" + user.Username + "/" + model.Name}, + input: tuple{"group-" + group.Name + "#member", "writer", "model-" + controller.Name + ":" + user.Name + "/" + model.Name}, want: createTuple( "group:"+stringGroupID(group.ID)+"#member", "writer", @@ -426,7 +426,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { }, // Test group -> applicationoffer by name { - input: tuple{"group-" + group.Name + "#member", "consumer", "applicationoffer-" + controller.Name + ":" + user.Username + "/" + model.Name + "." + offer.Name}, + input: tuple{"group-" + group.Name + "#member", "consumer", "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name}, want: createTuple( "group:"+stringGroupID(group.ID)+"#member", "consumer", @@ -532,9 +532,9 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { Relation: "administrator", Target: ofganames.ConvertTag(controller.ResourceTag()), }, - toRemove: tuple{"user-" + user.Username, "administrator", "controller-" + controller.Name}, + toRemove: tuple{"user-" + user.Name, "administrator", "controller-" + controller.Name}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "administrator", "controller:"+controller.UUID, ), @@ -548,9 +548,9 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { Relation: "administrator", Target: ofganames.ConvertTag(controller.ResourceTag()), }, - toRemove: tuple{"user-" + user.Username, "administrator", "controller-" + controller.UUID}, + toRemove: tuple{"user-" + user.Name, "administrator", "controller-" + controller.UUID}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "administrator", "controller:"+controller.UUID, ), @@ -564,9 +564,9 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { Relation: "member", Target: ofganames.ConvertTag(group.ResourceTag()), }, - toRemove: tuple{"user-" + user.Username, "member", "group-" + group.Name}, + toRemove: tuple{"user-" + user.Name, "member", "group-" + group.Name}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "member", "group:"+stringGroupID(group.ID), ), @@ -596,9 +596,9 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { Relation: "writer", Target: ofganames.ConvertTag(model.ResourceTag()), }, - toRemove: tuple{"user-" + user.Username, "writer", "model-" + controller.Name + ":" + user.Username + "/" + model.Name}, + toRemove: tuple{"user-" + user.Name, "writer", "model-" + controller.Name + ":" + user.Name + "/" + model.Name}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "writer", "model:"+model.UUID.String, ), @@ -612,9 +612,9 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { Relation: "writer", Target: ofganames.ConvertTag(model.ResourceTag()), }, - toRemove: tuple{"user-" + user.Username, "writer", "model-" + model.UUID.String}, + toRemove: tuple{"user-" + user.Name, "writer", "model-" + model.UUID.String}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "writer", "model:"+model.UUID.String, ), @@ -628,9 +628,9 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { Relation: "consumer", Target: ofganames.ConvertTag(offer.ResourceTag()), }, - toRemove: tuple{"user-" + user.Username, "consumer", "applicationoffer-" + controller.Name + ":" + user.Username + "/" + model.Name + "." + offer.Name}, + toRemove: tuple{"user-" + user.Name, "consumer", "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "consumer", "applicationoffer:"+offer.UUID, ), @@ -644,9 +644,9 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { Relation: "consumer", Target: ofganames.ConvertTag(offer.ResourceTag()), }, - toRemove: tuple{"user-" + user.Username, "consumer", "applicationoffer-" + offer.UUID}, + toRemove: tuple{"user-" + user.Name, "consumer", "applicationoffer-" + offer.UUID}, want: createTuple( - "user:"+user.Username, + "user:"+user.Name, "consumer", "applicationoffer:"+offer.UUID, ), @@ -692,7 +692,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { Relation: "writer", Target: ofganames.ConvertTag(model.ResourceTag()), }, - toRemove: tuple{"group-" + group.Name + "#member", "writer", "model-" + controller.Name + ":" + user.Username + "/" + model.Name}, + toRemove: tuple{"group-" + group.Name + "#member", "writer", "model-" + controller.Name + ":" + user.Name + "/" + model.Name}, want: createTuple( "group:"+stringGroupID(group.ID)+"#member", "writer", @@ -724,7 +724,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { Relation: "consumer", Target: ofganames.ConvertTag(offer.ResourceTag()), }, - toRemove: tuple{"group-" + group.Name + "#member", "consumer", "applicationoffer-" + controller.Name + ":" + user.Username + "/" + model.Name + "." + offer.Name}, + toRemove: tuple{"group-" + group.Name + "#member", "consumer", "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name}, want: createTuple( "group:"+stringGroupID(group.ID)+"#member", "consumer", @@ -803,16 +803,16 @@ func (s *accessControlSuite) TestJAASTag(c *gc.C) { expectedError string }{{ tag: ofganames.ConvertTag(user.ResourceTag()), - expectedJAASTag: "user-" + user.Username, + expectedJAASTag: "user-" + user.Name, }, { tag: ofganames.ConvertTag(controller.ResourceTag()), expectedJAASTag: "controller-" + controller.Name, }, { tag: ofganames.ConvertTag(model.ResourceTag()), - expectedJAASTag: "model-" + controller.Name + ":" + user.Username + "/" + model.Name, + expectedJAASTag: "model-" + controller.Name + ":" + user.Name + "/" + model.Name, }, { tag: ofganames.ConvertTag(applicationOffer.ResourceTag()), - expectedJAASTag: "applicationoffer-" + controller.Name + ":" + user.Username + "/" + model.Name + "." + applicationOffer.Name, + expectedJAASTag: "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + applicationOffer.Name, }, { tag: &ofganames.Tag{}, expectedError: "unexpected tag kind: ", @@ -846,7 +846,7 @@ func (s *accessControlSuite) TestListRelationshipTuples(c *gc.C) { Relation: "member", TargetObject: "group-yellow", }, { - Object: "user-" + user.Username, + Object: "user-" + user.Name, Relation: "member", TargetObject: "group-orange", }, { @@ -856,7 +856,7 @@ func (s *accessControlSuite) TestListRelationshipTuples(c *gc.C) { }, { Object: "group-orange#member", Relation: "administrator", - TargetObject: "applicationoffer-" + controller.Name + ":" + user.Username + "/" + model.Name + "." + applicationOffer.Name, + TargetObject: "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + applicationOffer.Name, }} err = client.AddRelation(&apiparams.AddRelationRequest{Tuples: tuples}) @@ -869,7 +869,7 @@ func (s *accessControlSuite) TestListRelationshipTuples(c *gc.C) { response, err = client.ListRelationshipTuples(&apiparams.ListRelationshipTuplesRequest{ Tuple: apiparams.RelationshipTuple{ - TargetObject: "applicationoffer-" + controller.Name + ":" + user.Username + "/" + model.Name + "." + applicationOffer.Name, + TargetObject: "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + applicationOffer.Name, }, }) c.Assert(err, jc.ErrorIsNil) @@ -918,8 +918,8 @@ func (s *accessControlSuite) TestCheckRelationOfferReaderFlow(c *gc.C) { offerTag := ofganames.ConvertTag(offer.ResourceTag()) // JAAS style keys, to be translated and checked against UUIDs/users/groups - userJAASKey := "user-" + user.Username - offerJAASKey := "applicationoffer-" + controller.Name + ":" + user.Username + "/" + model.Name + "." + offer.Name + userJAASKey := "user-" + user.Name + offerJAASKey := "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name // Test direct relation to an applicationoffer from a user of a group via "reader" relation @@ -989,8 +989,8 @@ func (s *accessControlSuite) TestCheckRelationOfferConsumerFlow(c *gc.C) { offerTag := ofganames.ConvertTag(offer.ResourceTag()) // JAAS style keys, to be translated and checked against UUIDs/users/groups - userJAASKey := "user-" + user.Username - offerJAASKey := "applicationoffer-" + controller.Name + ":" + user.Username + "/" + model.Name + "." + offer.Name + userJAASKey := "user-" + user.Name + offerJAASKey := "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name // Test direct relation to an applicationoffer from a user of a group via "consumer" relation userToGroupMember := openfga.Tuple{ @@ -1060,8 +1060,8 @@ func (s *accessControlSuite) TestCheckRelationModelReaderFlow(c *gc.C) { // Test direct relation to a model from a user of a group via "writer" relation // JAAS style keys, to be translated and checked against UUIDs/users/groups - userJAASKey := "user-" + user.Username - modelJAASKey := "model-" + controller.Name + ":" + user.Username + "/" + model.Name + userJAASKey := "user-" + user.Name + modelJAASKey := "model-" + controller.Name + ":" + user.Name + "/" + model.Name // Test direct relation to a model from a user of a group via "reader" relation userToGroupMember := openfga.Tuple{ @@ -1141,8 +1141,8 @@ func (s *accessControlSuite) TestCheckRelationModelWriterFlow(c *gc.C) { } // Make group members writer of model via member union // JAAS style keys, to be translated and checked against UUIDs/users/groups - userJAASKey := "user-" + user.Username - modelJAASKey := "model-" + controller.Name + ":" + user.Username + "/" + model.Name + userJAASKey := "user-" + user.Name + modelJAASKey := "model-" + controller.Name + ":" + user.Name + "/" + model.Name err := ofgaClient.AddRelation( ctx, @@ -1200,11 +1200,11 @@ func (s *accessControlSuite) TestCheckRelationControllerAdministratorFlow(c *gc. offerTag := ofganames.ConvertTag(offer.ResourceTag()) // JAAS style keys, to be translated and checked against UUIDs/users/groups - userJAASKey := "user-" + user.Username + userJAASKey := "user-" + user.Name groupJAASKey := "group-" + group.Name controllerJAASKey := "controller-" + controller.Name - modelJAASKey := "model-" + controller.Name + ":" + user.Username + "/" + model.Name - offerJAASKey := "applicationoffer-" + controller.Name + ":" + user.Username + "/" + model.Name + "." + offer.Name + modelJAASKey := "model-" + controller.Name + ":" + user.Name + "/" + model.Name + offerJAASKey := "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name // Test the administrator flow of a group user being related to a controller via administrator relation userToGroup := openfga.Tuple{ @@ -1412,7 +1412,7 @@ func (s *accessControlSuite) TestResolveTupleObjectMapsModelUUIDs(c *gc.C) { user, _, controller, model, _, _, _, _, closeClient := createTestControllerEnvironment(ctx, c, s) defer closeClient() - jimmTag := "model-" + controller.Name + ":" + user.Username + "/" + model.Name + "#administrator" + jimmTag := "model-" + controller.Name + ":" + user.Name + "/" + model.Name + "#administrator" tag, err := jujuapi.ResolveTag(s.JIMM.UUID, s.JIMM.DB(), jimmTag) c.Assert(err, gc.IsNil) @@ -1426,7 +1426,7 @@ func (s *accessControlSuite) TestResolveTupleObjectMapsApplicationOffersUUIDs(c user, _, controller, model, offer, _, _, _, closeClient := createTestControllerEnvironment(ctx, c, s) closeClient() - jimmTag := "applicationoffer-" + controller.Name + ":" + user.Username + "/" + model.Name + "." + offer.Name + "#administrator" + jimmTag := "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name + "#administrator" jujuTag, err := jujuapi.ResolveTag(s.JIMM.UUID, s.JIMM.DB(), jimmTag) c.Assert(err, gc.IsNil) @@ -1447,7 +1447,7 @@ func (s *accessControlSuite) TestParseTag(c *gc.C) { user, _, controller, model, _, _, _, _, closeClient := createTestControllerEnvironment(ctx, c, s) defer closeClient() - jimmTag := "model-" + controller.Name + ":" + user.Username + "/" + model.Name + "#administrator" + jimmTag := "model-" + controller.Name + ":" + user.Name + "/" + model.Name + "#administrator" // JIMM tag syntax for models tag, err := jujuapi.ParseTag(ctx, s.JIMM.UUID, s.JIMM.DB(), jimmTag) @@ -1502,7 +1502,7 @@ func createTestControllerEnvironment(ctx context.Context, c *gc.C, s *accessCont c.Assert(err, gc.IsNil) u := dbmodel.Identity{ - Username: petname.Generate(2, "-") + "@external", + Name: petname.Generate(2, "-") + "@external", } c.Assert(db.DB.Create(&u).Error, gc.IsNil) @@ -1531,7 +1531,7 @@ func createTestControllerEnvironment(ctx context.Context, c *gc.C, s *accessCont cred := dbmodel.CloudCredential{ Name: petname.Generate(2, "-"), CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) @@ -1543,7 +1543,7 @@ func createTestControllerEnvironment(ctx context.Context, c *gc.C, s *accessCont String: id.String(), Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1561,7 +1561,7 @@ func createTestControllerEnvironment(ctx context.Context, c *gc.C, s *accessCont c.Assert(err, gc.IsNil) offerName := petname.Generate(2, "-") - offerURL, err := crossmodel.ParseOfferURL(controller.Name + ":" + u.Username + "/" + model.Name + "." + offerName) + offerURL, err := crossmodel.ParseOfferURL(controller.Name + ":" + u.Name + "/" + model.Name + "." + offerName) c.Assert(err, gc.IsNil) offer := dbmodel.ApplicationOffer{ diff --git a/internal/jujuapi/applicationoffers.go b/internal/jujuapi/applicationoffers.go index 70930a00f..ce95ae114 100644 --- a/internal/jujuapi/applicationoffers.go +++ b/internal/jujuapi/applicationoffers.go @@ -110,7 +110,7 @@ func (r *controllerRoot) getConsumeDetails(ctx context.Context, user *openfga.Us // Ensure the path is normalised. if ourl.User == "" { // If the model owner is not specified use the specified user. - ourl.User = user.Username + ourl.User = user.Name } details := jujuparams.ConsumeOfferDetails{ diff --git a/internal/jujuapi/applicationoffers_test.go b/internal/jujuapi/applicationoffers_test.go index bb5855987..f57bc843c 100644 --- a/internal/jujuapi/applicationoffers_test.go +++ b/internal/jujuapi/applicationoffers_test.go @@ -349,7 +349,7 @@ func (s *applicationOffersSuite) TestDestroyOffers(c *gc.C) { } err = s.JIMM.Database.GetApplicationOffer(context.Background(), &offer) c.Assert(err, gc.Equals, nil) - charlie := openfga.NewUser(&dbmodel.Identity{Username: "charlie@external"}, s.OFGAClient) + charlie := openfga.NewUser(&dbmodel.Identity{Name: "charlie@external"}, s.OFGAClient) err = charlie.SetApplicationOfferAccess(context.Background(), offer.ResourceTag(), ofganames.ReaderRelation) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/cloud.go b/internal/jujuapi/cloud.go index c0dd4c56f..225a0f9b8 100644 --- a/internal/jujuapi/cloud.go +++ b/internal/jujuapi/cloud.go @@ -333,7 +333,7 @@ func (r *controllerRoot) CredentialContents(ctx context.Context, args jujuparams results := make([]jujuparams.CredentialContentResult, len(args.Credentials)) for i, arg := range args.Credentials { - cct := names.NewCloudCredentialTag(fmt.Sprintf("%s/%s/%s", arg.CloudName, r.user.Username, arg.CredentialName)) + cct := names.NewCloudCredentialTag(fmt.Sprintf("%s/%s/%s", arg.CloudName, r.user.Name, arg.CredentialName)) cred, err := r.jimm.GetCloudCredential(ctx, r.user, cct) if err != nil { results[i].Error = mapError(errors.E(op, err)) diff --git a/internal/jujuapi/cloud_test.go b/internal/jujuapi/cloud_test.go index 027d34dd7..6105a414f 100644 --- a/internal/jujuapi/cloud_test.go +++ b/internal/jujuapi/cloud_test.go @@ -946,13 +946,13 @@ func (s *cloudSuite) TestListCloudInfo(c *gc.C) { err = client.GrantCloud("bob@external", "add-model", "test-cloud") c.Assert(err, gc.Equals, nil) */ - bob := openfga.NewUser(&dbmodel.Identity{Username: "bob@external"}, s.OFGAClient) + bob := openfga.NewUser(&dbmodel.Identity{Name: "bob@external"}, s.OFGAClient) err = bob.SetCloudAccess(context.Background(), names.NewCloudTag("test-cloud"), ofganames.CanAddModelRelation) c.Assert(err, gc.Equals, nil) err = bob.SetCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), ofganames.CanAddModelRelation) c.Assert(err, gc.Equals, nil) - alice := openfga.NewUser(&dbmodel.Identity{Username: "alice@external"}, s.OFGAClient) + alice := openfga.NewUser(&dbmodel.Identity{Name: "alice@external"}, s.OFGAClient) err = alice.SetCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), ofganames.CanAddModelRelation) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index 5f5a002c2..8162b2b09 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -147,7 +147,7 @@ func (r *controllerRoot) masquerade(ctx context.Context, userTag string) (*openf return nil, errors.E(errors.CodeUnauthorized, "unauthorized") } user := dbmodel.Identity{ - Username: ut.Id(), + Name: ut.Id(), } if err := r.jimm.DB().GetUser(ctx, &user); err != nil { return nil, err diff --git a/internal/jujuapi/jimm_test.go b/internal/jujuapi/jimm_test.go index efb468b80..5f1b724a9 100644 --- a/internal/jujuapi/jimm_test.go +++ b/internal/jujuapi/jimm_test.go @@ -568,7 +568,7 @@ func (s *jimmSuite) TestImportModel(c *gc.C) { func (s *jimmSuite) TestAddCloudToController(c *gc.C) { ctx := context.Background() u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } err := s.JIMM.Database.GetUser(ctx, &u) c.Assert(err, gc.IsNil) @@ -605,7 +605,7 @@ func (s *jimmSuite) TestAddCloudToController(c *gc.C) { func (s *jimmSuite) TestAddExistingCloudToController(c *gc.C) { ctx := context.Background() u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } err := s.JIMM.Database.GetUser(ctx, &u) c.Assert(err, gc.IsNil) @@ -653,7 +653,7 @@ func (s *jimmSuite) TestAddExistingCloudToController(c *gc.C) { func (s *jimmSuite) TestRemoveCloudFromController(c *gc.C) { ctx := context.Background() u := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } err := s.JIMM.Database.GetUser(ctx, &u) c.Assert(err, gc.IsNil) diff --git a/internal/jujuapi/modelmanager_test.go b/internal/jujuapi/modelmanager_test.go index e3f06c0f4..35667e163 100644 --- a/internal/jujuapi/modelmanager_test.go +++ b/internal/jujuapi/modelmanager_test.go @@ -264,7 +264,7 @@ func (s *modelManagerSuite) TestModelInfo(c *gc.C) { //client := modelmanager.NewClient(conn) //err := client.GrantModel("bob@external", "write", mt4.Id()) //c.Assert(err, gc.Equals, nil) - bob := openfga.NewUser(&dbmodel.Identity{Username: "bob@external"}, s.OFGAClient) + bob := openfga.NewUser(&dbmodel.Identity{Name: "bob@external"}, s.OFGAClient) err := bob.SetModelAccess(context.Background(), mt4, ofganames.WriterRelation) c.Assert(err, gc.Equals, nil) @@ -521,7 +521,7 @@ func (s *modelManagerSuite) TestModelInfoDisableControllerUUIDMasking(c *gc.C) { s.Candid.AddUser("bob", "controller-admin") // we make bob a jimm administrator - bob := openfga.NewUser(&dbmodel.Identity{Username: "bob@external"}, s.OFGAClient) + bob := openfga.NewUser(&dbmodel.Identity{Name: "bob@external"}, s.OFGAClient) err = bob.SetControllerAccess(context.Background(), s.JIMM.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/usermanager.go b/internal/jujuapi/usermanager.go index dfd574f70..c31491e80 100644 --- a/internal/jujuapi/usermanager.go +++ b/internal/jujuapi/usermanager.go @@ -83,7 +83,7 @@ func (r *controllerRoot) userInfo(ctx context.Context, entity string) (*jujupara if err != nil { return nil, errors.E(op, err, errors.CodeBadRequest) } - if r.user.Username != user.Id() { + if r.user.Name != user.Id() { return nil, errors.E(op, errors.CodeUnauthorized) } ui := r.user.ToJujuUserInfo() diff --git a/internal/jujuapi/websocket_test.go b/internal/jujuapi/websocket_test.go index 478da9dc8..0b7aa1d23 100644 --- a/internal/jujuapi/websocket_test.go +++ b/internal/jujuapi/websocket_test.go @@ -88,7 +88,7 @@ func (s *websocketSuite) SetUpTest(c *gc.C) { bob := openfga.NewUser( &dbmodel.Identity{ - Username: "bob@external", + Name: "bob@external", }, s.OFGAClient, ) @@ -175,7 +175,7 @@ func (s *proxySuite) TestConnectToModel(c *gc.C) { func (s *proxySuite) TestConnectToModelAndLogin(c *gc.C) { ctx := context.Background() alice := names.NewUserTag("alice") - aliceUser := openfga.NewUser(&dbmodel.Identity{Username: alice.Id()}, s.JIMM.OpenFGAClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.JIMM.OpenFGAClient) err := aliceUser.SetControllerAccess(ctx, s.Model.Controller.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, gc.IsNil) conn, err := s.openNoAssert(c, &api.Info{ diff --git a/internal/openfga/user.go b/internal/openfga/user.go index 34d185329..9de3d3f6c 100644 --- a/internal/openfga/user.go +++ b/internal/openfga/user.go @@ -318,7 +318,7 @@ func IsAdministrator[T administratorT](ctx context.Context, u *User, resource T) ctx, "openfga administrator check failed", zap.Error(err), - zap.String("user", u.Username), + zap.String("user", u.Name), zap.String("resource", resource.String()), ) return false, errors.E(err) @@ -440,7 +440,7 @@ func ListUsersWithAccess[T ofganames.ResourceTagger](ctx context.Context, client if entity.ID == "*" { entity.ID = ofganames.EveryoneUser } - users[i] = NewUser(&dbmodel.Identity{Username: entity.ID}, client) + users[i] = NewUser(&dbmodel.Identity{Name: entity.ID}, client) } return users, nil } diff --git a/internal/openfga/user_test.go b/internal/openfga/user_test.go index e4a14670e..57e55df83 100644 --- a/internal/openfga/user_test.go +++ b/internal/openfga/user_test.go @@ -55,7 +55,7 @@ func (s *userTestSuite) TestIsAdministrator(c *gc.C) { u := openfga.NewUser( &dbmodel.Identity{ - Username: user.Id(), + Name: user.Id(), }, s.ofgaClient, ) @@ -102,9 +102,9 @@ func (s *userTestSuite) TestModelAccess(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.Identity{Username: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.Identity{Username: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.Identity{Username: alice.Id()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Name: "adam"}, s.ofgaClient) + eveUser := openfga.NewUser(&dbmodel.Identity{Name: eve.Id()}, s.ofgaClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.ofgaClient) relation := eveUser.GetModelAccess(ctx, model) c.Assert(relation, gc.DeepEquals, ofganames.AdministratorRelation) @@ -137,9 +137,9 @@ func (s *userTestSuite) TestSetModelAccess(c *gc.C) { eve := names.NewUserTag("eve") alice := names.NewUserTag("alice") - adamUser := openfga.NewUser(&dbmodel.Identity{Username: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.Identity{Username: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.Identity{Username: alice.Id()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Name: "adam"}, s.ofgaClient) + eveUser := openfga.NewUser(&dbmodel.Identity{Name: eve.Id()}, s.ofgaClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.ofgaClient) err = eveUser.SetModelAccess(ctx, model, ofganames.AdministratorRelation) c.Assert(err, gc.IsNil) @@ -197,9 +197,9 @@ func (s *userTestSuite) TestCloudAccess(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.Identity{Username: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.Identity{Username: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.Identity{Username: alice.Id()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Name: "adam"}, s.ofgaClient) + eveUser := openfga.NewUser(&dbmodel.Identity{Name: eve.Id()}, s.ofgaClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.ofgaClient) relation := eveUser.GetCloudAccess(ctx, cloud) c.Assert(relation, gc.DeepEquals, ofganames.AdministratorRelation) @@ -220,9 +220,9 @@ func (s *userTestSuite) TestSetCloudAccess(c *gc.C) { eve := names.NewUserTag("eve") alice := names.NewUserTag("alice") - adamUser := openfga.NewUser(&dbmodel.Identity{Username: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.Identity{Username: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.Identity{Username: alice.Id()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Name: "adam"}, s.ofgaClient) + eveUser := openfga.NewUser(&dbmodel.Identity{Name: eve.Id()}, s.ofgaClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.ofgaClient) err = eveUser.SetCloudAccess(ctx, cloud, ofganames.AdministratorRelation) c.Assert(err, gc.IsNil) @@ -273,9 +273,9 @@ func (s *userTestSuite) TestControllerAccess(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.Identity{Username: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.Identity{Username: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.Identity{Username: alice.Id()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Name: "adam"}, s.ofgaClient) + eveUser := openfga.NewUser(&dbmodel.Identity{Name: eve.Id()}, s.ofgaClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.ofgaClient) relation := eveUser.GetControllerAccess(ctx, controller) c.Assert(relation, gc.DeepEquals, ofganames.AdministratorRelation) @@ -302,9 +302,9 @@ func (s *userTestSuite) TestSetControllerAccess(c *gc.C) { eve := names.NewUserTag("eve") alice := names.NewUserTag("alice") - adamUser := openfga.NewUser(&dbmodel.Identity{Username: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.Identity{Username: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.Identity{Username: alice.Id()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Name: "adam"}, s.ofgaClient) + eveUser := openfga.NewUser(&dbmodel.Identity{Name: eve.Id()}, s.ofgaClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.ofgaClient) err = eveUser.SetControllerAccess(ctx, controller, ofganames.AdministratorRelation) c.Assert(err, gc.IsNil) @@ -339,7 +339,7 @@ func (s *userTestSuite) TestUnsetAuditLogViewerAccess(c *gc.C) { c.Assert(err, gc.IsNil) controller := names.NewControllerTag(controllerUUID.String()) - aliceUser := openfga.NewUser(&dbmodel.Identity{Username: "alice"}, s.ofgaClient) + aliceUser := openfga.NewUser(&dbmodel.Identity{Name: "alice"}, s.ofgaClient) tuples := []openfga.Tuple{{ Object: ofganames.ConvertTag(aliceUser.Identity.ResourceTag()), @@ -425,7 +425,7 @@ func (s *userTestSuite) TestListRelatedUsers(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - eveUser := openfga.NewUser(&dbmodel.Identity{Username: "eve"}, s.ofgaClient) + eveUser := openfga.NewUser(&dbmodel.Identity{Name: "eve"}, s.ofgaClient) isAdministrator, err := openfga.IsAdministrator(ctx, eveUser, offer) c.Assert(err, gc.IsNil) c.Assert(isAdministrator, gc.Equals, true) @@ -473,7 +473,7 @@ func (s *userTestSuite) TestListModels(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.Identity{Username: adam.Name()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Name: adam.Name()}, s.ofgaClient) modelUUIDs, err := adamUser.ListModels(ctx, ofganames.ReaderRelation) c.Assert(err, gc.IsNil) wantUUIDs := []string{model1UUID.String(), model2UUID.String(), model3UUID.String()} @@ -515,7 +515,7 @@ func (s *userTestSuite) TestListApplicationOffers(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.Identity{Username: adam.Name()}, s.ofgaClient) + adamUser := openfga.NewUser(&dbmodel.Identity{Name: adam.Name()}, s.ofgaClient) offerUUIDs, err := adamUser.ListApplicationOffers(ctx, ofganames.ReaderRelation) c.Assert(err, gc.IsNil) wantUUIDs := []string{offer1UUID.String(), offer2UUID.String(), offer3UUID.String()} diff --git a/local/seed_db/main.go b/local/seed_db/main.go index dd180b8a3..c8f0af37a 100644 --- a/local/seed_db/main.go +++ b/local/seed_db/main.go @@ -48,7 +48,7 @@ func main() { } u := dbmodel.Identity{ - Username: petname.Generate(2, "-") + "@external", + Name: petname.Generate(2, "-") + "@external", } if err = db.DB.Create(&u).Error; err != nil { fmt.Println("failed to add user to db ", err) @@ -86,7 +86,7 @@ func main() { cred := dbmodel.CloudCredential{ Name: petname.Generate(2, "-"), CloudName: cloud.Name, - OwnerUsername: u.Username, + OwnerUsername: u.Name, AuthType: "empty", } if err = db.SetCloudCredential(ctx, &cred); err != nil { @@ -100,7 +100,7 @@ func main() { String: id.String(), Valid: true, }, - OwnerUsername: u.Username, + OwnerUsername: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -119,7 +119,7 @@ func main() { } offerName := petname.Generate(2, "-") - offerURL, _ := crossmodel.ParseOfferURL(controller.Name + ":" + u.Username + "/" + model.Name + "." + offerName) + offerURL, _ := crossmodel.ParseOfferURL(controller.Name + ":" + u.Name + "/" + model.Name + "." + offerName) offer := dbmodel.ApplicationOffer{ UUID: id.String(), Name: offerName, diff --git a/service.go b/service.go index b455431f4..66a972d26 100644 --- a/service.go +++ b/service.go @@ -557,7 +557,7 @@ func ensureControllerAdministrators(ctx context.Context, client *openfga.OFGACli tuples := []openfga.Tuple{} for _, username := range admins { userTag := names.NewUserTag(username) - user := openfga.NewUser(&dbmodel.Identity{Username: userTag.Id()}, client) + user := openfga.NewUser(&dbmodel.Identity{Name: userTag.Id()}, client) isAdmin, err := openfga.IsAdministrator(ctx, user, controller) if err != nil { return errors.E(err) diff --git a/service_test.go b/service_test.go index ab0a2dc19..c3319dff6 100644 --- a/service_test.go +++ b/service_test.go @@ -252,7 +252,7 @@ func TestOpenFGA(t *testing.T) { // assert controller admins have been created in openfga for _, username := range []string{"alice", "eve"} { user := openfga.NewUser( - &dbmodel.Identity{Username: username}, + &dbmodel.Identity{Name: username}, client, ) allowed, err := openfga.IsAdministrator(context.Background(), user, names.NewControllerTag(p.ControllerUUID)) @@ -297,7 +297,7 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { Name: "test-application-offer", } user := dbmodel.Identity{ - Username: "alice@external", + Name: "alice@external", } ctx := context.Background() From 24f6f280725d9ad72de37bd14e5c124a90771638 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Jan 2024 19:56:31 +0000 Subject: [PATCH 009/126] Update foreign-key tag Signed-off-by: Babak K. Shandiz --- internal/dbmodel/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/dbmodel/user.go b/internal/dbmodel/user.go index 070d2b5f3..56ebe0bf6 100644 --- a/internal/dbmodel/user.go +++ b/internal/dbmodel/user.go @@ -33,7 +33,7 @@ type Identity struct { Disabled bool `gorm:"not null;default:FALSE"` // CloudCredentials are the cloud credentials owned by this identity. - CloudCredentials []CloudCredential `gorm:"foreignKey:OwnerUsername;references:Username"` + CloudCredentials []CloudCredential `gorm:"foreignKey:OwnerIdentityName;references:Name"` } // Tag returns a names.Tag for the identity. From aa1040ac96c97543e4e5c37d9e30f30588998012 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Jan 2024 20:04:59 +0000 Subject: [PATCH 010/126] Rename `CloudCredential.OwnerUsername` to `OwnerIdentityName` Signed-off-by: Babak K. Shandiz --- .../cmd/importcloudcredentials_test.go | 18 +-- cmd/jimmctl/cmd/relation_test.go | 16 +-- internal/db/cloudcredential.go | 12 +- internal/db/cloudcredential_test.go | 44 ++++---- internal/db/user_test.go | 16 +-- internal/dbmodel/cloudcredential.go | 12 +- internal/dbmodel/cloudcredential_test.go | 10 +- internal/dbmodel/user_test.go | 20 ++-- internal/jimm/applicationoffer_test.go | 80 ++++++------- internal/jimm/cloudcredential.go | 4 +- internal/jimm/cloudcredential_test.go | 106 +++++++++--------- internal/jimm/model.go | 6 +- internal/jimmtest/env.go | 2 +- internal/jujuapi/access_control_test.go | 8 +- local/seed_db/main.go | 8 +- 15 files changed, 181 insertions(+), 181 deletions(-) diff --git a/cmd/jimmctl/cmd/importcloudcredentials_test.go b/cmd/jimmctl/cmd/importcloudcredentials_test.go index da7793fc4..69407397e 100644 --- a/cmd/jimmctl/cmd/importcloudcredentials_test.go +++ b/cmd/jimmctl/cmd/importcloudcredentials_test.go @@ -67,27 +67,27 @@ func (s *importCloudCredentialsSuite) TestImportCloudCredentials(c *gc.C) { c.Assert(err, gc.IsNil) cred1 := dbmodel.CloudCredential{ - CloudName: "aws", - OwnerUsername: "alice@external", - Name: "test1", + CloudName: "aws", + OwnerIdentityName: "alice@external", + Name: "test1", } err = s.JIMM.Database.GetCloudCredential(context.Background(), &cred1) c.Assert(err, gc.IsNil) c.Check(cred1.AuthType, gc.Equals, "access-key") cred2 := dbmodel.CloudCredential{ - CloudName: "aws", - OwnerUsername: "bob@external", - Name: "test1", + CloudName: "aws", + OwnerIdentityName: "bob@external", + Name: "test1", } err = s.JIMM.Database.GetCloudCredential(context.Background(), &cred2) c.Assert(err, gc.IsNil) c.Check(cred2.AuthType, gc.Equals, "access-key") cred3 := dbmodel.CloudCredential{ - CloudName: "gce", - OwnerUsername: "charlie@external", - Name: "test1", + CloudName: "gce", + OwnerIdentityName: "charlie@external", + Name: "test1", } err = s.JIMM.Database.GetCloudCredential(context.Background(), &cred3) c.Assert(err, gc.IsNil) diff --git a/cmd/jimmctl/cmd/relation_test.go b/cmd/jimmctl/cmd/relation_test.go index 89324ede5..fc051764b 100644 --- a/cmd/jimmctl/cmd/relation_test.go +++ b/cmd/jimmctl/cmd/relation_test.go @@ -300,10 +300,10 @@ func initializeEnvironment(c *gc.C, ctx context.Context, db *db.Database, u dbmo env.controllers = []dbmodel.Controller{controller} cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) c.Assert(err, gc.Equals, nil) @@ -476,10 +476,10 @@ func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { c.Assert(err, gc.IsNil) cred := dbmodel.CloudCredential{ - Name: petname.Generate(2, "-"), - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: petname.Generate(2, "-"), + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) c.Assert(err, gc.IsNil) diff --git a/internal/db/cloudcredential.go b/internal/db/cloudcredential.go index bb04f4b02..b48ce8997 100644 --- a/internal/db/cloudcredential.go +++ b/internal/db/cloudcredential.go @@ -19,8 +19,8 @@ func (d *Database) SetCloudCredential(ctx context.Context, cred *dbmodel.CloudCr return errors.E(op, err) } - if cred.CloudName == "" || cred.OwnerUsername == "" || cred.Name == "" { - return errors.E(op, errors.CodeBadRequest, fmt.Sprintf("invalid cloudcredential tag %q", cred.CloudName+"/"+cred.OwnerUsername+"/"+cred.Name)) + if cred.CloudName == "" || cred.OwnerIdentityName == "" || cred.Name == "" { + return errors.E(op, errors.CodeBadRequest, fmt.Sprintf("invalid cloudcredential tag %q", cred.CloudName+"/"+cred.OwnerIdentityName+"/"+cred.Name)) } db := d.DB.WithContext(ctx) @@ -44,16 +44,16 @@ func (d *Database) GetCloudCredential(ctx context.Context, cred *dbmodel.CloudCr if err := d.ready(); err != nil { return errors.E(op, err) } - if cred.CloudName == "" || cred.OwnerUsername == "" || cred.Name == "" { - return errors.E(op, errors.CodeNotFound, fmt.Sprintf("cloudcredential %q not found", cred.CloudName+"/"+cred.OwnerUsername+"/"+cred.Name)) + if cred.CloudName == "" || cred.OwnerIdentityName == "" || cred.Name == "" { + return errors.E(op, errors.CodeNotFound, fmt.Sprintf("cloudcredential %q not found", cred.CloudName+"/"+cred.OwnerIdentityName+"/"+cred.Name)) } db := d.DB.WithContext(ctx) db = db.Preload("Cloud") db = db.Preload("Models") - if err := db.Where("cloud_name = ? AND owner_username = ? AND name = ?", cred.CloudName, cred.OwnerUsername, cred.Name).First(&cred).Error; err != nil { + if err := db.Where("cloud_name = ? AND owner_username = ? AND name = ?", cred.CloudName, cred.OwnerIdentityName, cred.Name).First(&cred).Error; err != nil { err := dbError(err) if errors.ErrorCode(err) == errors.CodeNotFound { - return errors.E(op, errors.CodeNotFound, fmt.Sprintf("cloudcredential %q not found", cred.CloudName+"/"+cred.OwnerUsername+"/"+cred.Name), err) + return errors.E(op, errors.CodeNotFound, fmt.Sprintf("cloudcredential %q not found", cred.CloudName+"/"+cred.OwnerIdentityName+"/"+cred.Name), err) } return errors.E(op, err) } diff --git a/internal/db/cloudcredential_test.go b/internal/db/cloudcredential_test.go index 77ae3778e..108ed1f67 100644 --- a/internal/db/cloudcredential_test.go +++ b/internal/db/cloudcredential_test.go @@ -45,12 +45,12 @@ func (s *dbSuite) TestSetCloudCredentialInvalidTag(c *qt.C) { c.Assert(s.Database.DB.Create(&cloud).Error, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: "test-cred", - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-cred", + OwnerIdentityName: u.Name, + AuthType: "empty", } err = s.Database.SetCloudCredential(context.Background(), &cred) - c.Check(err, qt.ErrorMatches, fmt.Sprintf("invalid cloudcredential tag %q", cred.CloudName+"/"+cred.OwnerUsername+"/"+cred.Name)) + c.Check(err, qt.ErrorMatches, fmt.Sprintf("invalid cloudcredential tag %q", cred.CloudName+"/"+cred.OwnerIdentityName+"/"+cred.Name)) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeBadRequest) } @@ -73,10 +73,10 @@ func (s *dbSuite) TestSetCloudCredential(c *qt.C) { c.Assert(s.Database.DB.Create(&cloud).Error, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: "test-cred", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-cred", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } c1 := cred err = s.Database.SetCloudCredential(context.Background(), &cred) @@ -110,10 +110,10 @@ func (s *dbSuite) TestSetCloudCredentialUpdate(c *qt.C) { c.Assert(s.Database.DB.Create(&cloud).Error, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: "test-cred", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-cred", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = s.Database.SetCloudCredential(context.Background(), &cred) c.Assert(err, qt.Equals, nil) @@ -135,9 +135,9 @@ func (s *dbSuite) TestSetCloudCredentialUpdate(c *qt.C) { c.Assert(err, qt.Equals, nil) dbCred := dbmodel.CloudCredential{ - CloudName: cloud.Name, - OwnerUsername: u.Name, - Name: cred.Name, + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + Name: cred.Name, } err = s.Database.GetCloudCredential(context.Background(), &dbCred) c.Assert(err, qt.Equals, nil) @@ -180,10 +180,10 @@ func (s *dbSuite) TestGetCloudCredential(c *qt.C) { c.Assert(s.Database.DB.Create(&cloud).Error, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: "test-cred", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-cred", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } cred.Cloud.Regions = nil err = s.Database.SetCloudCredential(context.Background(), &cred) @@ -193,9 +193,9 @@ func (s *dbSuite) TestGetCloudCredential(c *qt.C) { cred.Cloud.Regions = nil dbCred := dbmodel.CloudCredential{ - CloudName: cloud.Name, - OwnerUsername: u.Name, - Name: cred.Name, + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + Name: cred.Name, } err = s.Database.GetCloudCredential(context.Background(), &dbCred) c.Assert(err, qt.Equals, nil) diff --git a/internal/db/user_test.go b/internal/db/user_test.go index 93b09f27d..fbc910a2d 100644 --- a/internal/db/user_test.go +++ b/internal/db/user_test.go @@ -127,19 +127,19 @@ func (s *dbSuite) TestGetUserCloudCredentials(c *qt.C) { c.Assert(s.Database.DB.Create(&cloud).Error, qt.IsNil) cred1 := dbmodel.CloudCredential{ - Name: "test-cred-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-cred-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = s.Database.SetCloudCredential(context.Background(), &cred1) c.Assert(err, qt.Equals, nil) cred2 := dbmodel.CloudCredential{ - Name: "test-cred-2", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-cred-2", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = s.Database.SetCloudCredential(context.Background(), &cred2) c.Assert(err, qt.Equals, nil) diff --git a/internal/dbmodel/cloudcredential.go b/internal/dbmodel/cloudcredential.go index 3308d337d..cc6d0d524 100644 --- a/internal/dbmodel/cloudcredential.go +++ b/internal/dbmodel/cloudcredential.go @@ -21,9 +21,9 @@ type CloudCredential struct { CloudName string Cloud Cloud `gorm:"foreignKey:CloudName;references:Name;constraint:OnDelete:CASCADE"` - // Owner is the user that owns this credential. - OwnerUsername string - Owner Identity `gorm:"foreignKey:OwnerUsername;references:Username"` + // Owner is the identity that owns this credential. + OwnerIdentityName string + Owner Identity `gorm:"foreignKey:OwnerIdentityName;references:Name"` // AuthType is the type of the credential. AuthType string @@ -55,17 +55,17 @@ func (c CloudCredential) Tag() names.Tag { // a concrete type names.CloudCredentialTag instead of the // names.Tag interface. func (c CloudCredential) ResourceTag() names.CloudCredentialTag { - return names.NewCloudCredentialTag(fmt.Sprintf("%s/%s/%s", c.CloudName, c.OwnerUsername, c.Name)) + return names.NewCloudCredentialTag(fmt.Sprintf("%s/%s/%s", c.CloudName, c.OwnerIdentityName, c.Name)) } // SetTag sets the Name, CloudName and Username fields from the given tag. func (c *CloudCredential) SetTag(t names.CloudCredentialTag) { c.CloudName = t.Cloud().Id() c.Name = t.Name() - c.OwnerUsername = t.Owner().Id() + c.OwnerIdentityName = t.Owner().Id() } // Path returns a juju style cloud credential path. func (c CloudCredential) Path() string { - return fmt.Sprintf("%s/%s/%s", c.CloudName, c.OwnerUsername, c.Name) + return fmt.Sprintf("%s/%s/%s", c.CloudName, c.OwnerIdentityName, c.Name) } diff --git a/internal/dbmodel/cloudcredential_test.go b/internal/dbmodel/cloudcredential_test.go index d9e4340cc..651e89817 100644 --- a/internal/dbmodel/cloudcredential_test.go +++ b/internal/dbmodel/cloudcredential_test.go @@ -15,9 +15,9 @@ func TestCloudCredentialTag(t *testing.T) { c := qt.New(t) cred := dbmodel.CloudCredential{ - Name: "test-credential", - CloudName: "test-cloud", - OwnerUsername: "test-user", + Name: "test-credential", + CloudName: "test-cloud", + OwnerIdentityName: "test-user", } tag := cred.Tag() c.Check(tag.String(), qt.Equals, "cloudcred-test-cloud_test-user_test-credential") @@ -49,7 +49,7 @@ func TestCloudCredential(t *testing.T) { result := db.Create(&cred) c.Assert(result.Error, qt.IsNil) c.Check(cred.CloudName, qt.Equals, cred.Cloud.Name) - c.Check(cred.OwnerUsername, qt.Equals, cred.Owner.Name) + c.Check(cred.OwnerIdentityName, qt.Equals, cred.Owner.Name) } // TestCloudCredentialsCascadeOnDelete As of database version 1.3 (see migrations), @@ -77,7 +77,7 @@ func TestCloudCredentialsCascadeOnDelete(t *testing.T) { c.Assert(result.Error, qt.IsNil) c.Check(result.RowsAffected, qt.Equals, int64(1)) c.Check(cred.CloudName, qt.Equals, "test-cloud") - c.Check(cred.OwnerUsername, qt.Equals, "bob@external") + c.Check(cred.OwnerIdentityName, qt.Equals, "bob@external") result = db.Delete(&cloud) c.Assert(result.Error, qt.IsNil) diff --git a/internal/dbmodel/user_test.go b/internal/dbmodel/user_test.go index 2bd80b9ce..45d864892 100644 --- a/internal/dbmodel/user_test.go +++ b/internal/dbmodel/user_test.go @@ -104,17 +104,17 @@ func TestUserCloudCredentials(t *testing.T) { err := db.Model(u).Association("CloudCredentials").Find(&creds) c.Assert(err, qt.IsNil) c.Check(creds, qt.DeepEquals, []dbmodel.CloudCredential{{ - Model: cred1.Model, - Name: cred1.Name, - CloudName: cred1.CloudName, - OwnerUsername: cred1.OwnerUsername, - AuthType: cred1.AuthType, + Model: cred1.Model, + Name: cred1.Name, + CloudName: cred1.CloudName, + OwnerIdentityName: cred1.OwnerIdentityName, + AuthType: cred1.AuthType, }, { - Model: cred2.Model, - Name: cred2.Name, - CloudName: cred2.CloudName, - OwnerUsername: cred2.OwnerUsername, - AuthType: cred2.AuthType, + Model: cred2.Model, + Name: cred2.Name, + CloudName: cred2.CloudName, + OwnerIdentityName: cred2.OwnerIdentityName, + AuthType: cred2.AuthType, }}) } diff --git a/internal/jimm/applicationoffer_test.go b/internal/jimm/applicationoffer_test.go index 9a088149b..02bb5019b 100644 --- a/internal/jimm/applicationoffer_test.go +++ b/internal/jimm/applicationoffer_test.go @@ -122,10 +122,10 @@ var initializeEnvironment = func(c *qt.C, ctx context.Context, db *db.Database, c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) c.Assert(err, qt.IsNil) @@ -612,10 +612,10 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) c.Assert(err, qt.IsNil) @@ -980,10 +980,10 @@ func TestGetApplicationOffer(t *testing.T) { c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = j.Database.SetCloudCredential(ctx, &cred) c.Assert(err, qt.IsNil) @@ -1276,10 +1276,10 @@ func TestOffer(t *testing.T) { c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) c.Assert(err, qt.IsNil) @@ -1398,10 +1398,10 @@ func TestOffer(t *testing.T) { c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) c.Assert(err, qt.IsNil) @@ -1519,10 +1519,10 @@ func TestOffer(t *testing.T) { c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) c.Assert(err, qt.IsNil) @@ -1613,10 +1613,10 @@ func TestOffer(t *testing.T) { c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) c.Assert(err, qt.IsNil) @@ -1701,10 +1701,10 @@ func TestOffer(t *testing.T) { c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) c.Assert(err, qt.IsNil) @@ -1789,10 +1789,10 @@ func TestOffer(t *testing.T) { c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) c.Assert(err, qt.IsNil) @@ -1919,10 +1919,10 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) c.Assert(err, qt.IsNil) diff --git a/internal/jimm/cloudcredential.go b/internal/jimm/cloudcredential.go index 164102893..524b63f5e 100644 --- a/internal/jimm/cloudcredential.go +++ b/internal/jimm/cloudcredential.go @@ -321,11 +321,11 @@ func (j *JIMM) GetCloudCredentialAttributes(ctx context.Context, user *openfga.U if hidden { // Controller superusers cannot read hidden credential attributes. - if user.Name != cred.OwnerUsername { + if user.Name != cred.OwnerIdentityName { return nil, nil, errors.E(op, errors.CodeUnauthorized, "unauthorized") } } else { - if !user.JimmAdmin && user.Name != cred.OwnerUsername { + if !user.JimmAdmin && user.Name != cred.OwnerIdentityName { return nil, nil, errors.E(op, errors.CodeUnauthorized, "unauthorized") } } diff --git a/internal/jimm/cloudcredential_test.go b/internal/jimm/cloudcredential_test.go index 1bfd10525..164e22dc4 100644 --- a/internal/jimm/cloudcredential_test.go +++ b/internal/jimm/cloudcredential_test.go @@ -94,10 +94,10 @@ func TestUpdateCloudCredential(t *testing.T) { c.Assert(err, qt.Equals, nil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) c.Assert(err, qt.Equals, nil) @@ -209,10 +209,10 @@ func TestUpdateCloudCredential(t *testing.T) { c.Assert(err, qt.Equals, nil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) c.Assert(err, qt.Equals, nil) @@ -300,10 +300,10 @@ func TestUpdateCloudCredential(t *testing.T) { c.Assert(err, qt.Equals, nil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) c.Assert(err, qt.Equals, nil) @@ -394,10 +394,10 @@ func TestUpdateCloudCredential(t *testing.T) { c.Assert(err, qt.Equals, nil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: eve.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: eve.Name, + AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) c.Assert(err, qt.Equals, nil) @@ -443,7 +443,7 @@ func TestUpdateCloudCredential(t *testing.T) { Name: cloud.Name, Type: cloud.Type, }, - OwnerUsername: eve.Name, + OwnerIdentityName: eve.Name, Attributes: map[string]string{ "key1": "value1", "key2": "value2", @@ -508,10 +508,10 @@ func TestUpdateCloudCredential(t *testing.T) { c.Assert(err, qt.Equals, nil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) c.Assert(err, qt.Equals, nil) @@ -621,10 +621,10 @@ func TestUpdateCloudCredential(t *testing.T) { c.Assert(err, qt.Equals, nil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) c.Assert(err, qt.Equals, nil) @@ -805,9 +805,9 @@ func TestUpdateCloudCredential(t *testing.T) { c.Assert(result[0].ModelName, qt.Equals, "test-model") c.Assert(result[0].ModelUUID, qt.Equals, "00000001-0000-0000-0000-0000-000000000001") credential := dbmodel.CloudCredential{ - Name: expectedCredential.Name, - CloudName: expectedCredential.CloudName, - OwnerUsername: expectedCredential.OwnerUsername, + Name: expectedCredential.Name, + CloudName: expectedCredential.CloudName, + OwnerIdentityName: expectedCredential.OwnerIdentityName, } err = j.Database.GetCloudCredential(ctx, &credential) c.Assert(err, qt.Equals, nil) @@ -927,10 +927,10 @@ func TestRevokeCloudCredential(t *testing.T) { c.Assert(err, qt.Equals, nil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) c.Assert(err, qt.Equals, nil) @@ -1001,10 +1001,10 @@ func TestRevokeCloudCredential(t *testing.T) { c.Assert(err, qt.Equals, nil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) c.Assert(err, qt.Equals, nil) @@ -1071,10 +1071,10 @@ func TestRevokeCloudCredential(t *testing.T) { c.Assert(err, qt.Equals, nil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) c.Assert(err, qt.Equals, nil) @@ -1164,10 +1164,10 @@ func TestRevokeCloudCredential(t *testing.T) { c.Assert(err, qt.Equals, nil) cred := dbmodel.CloudCredential{ - Name: "test-credential-1", - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: "test-credential-1", + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) c.Assert(err, qt.Equals, nil) @@ -1342,8 +1342,8 @@ func TestGetCloudCredential(t *testing.T) { Name: cloud.Name, Type: cloud.Type, }, - OwnerUsername: u.Name, - AuthType: "empty", + OwnerIdentityName: u.Name, + AuthType: "empty", } err = j.Database.SetCloudCredential(context.Background(), &cred) c.Assert(err, qt.Equals, nil) @@ -1690,17 +1690,17 @@ func TestCloudCredentialAttributeStore(t *testing.T) { c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ - OwnerUsername: "alice@external", - Name: "cred-1", - CloudName: "test", + OwnerIdentityName: "alice@external", + Name: "cred-1", + CloudName: "test", } err = j.Database.GetCloudCredential(ctx, &cred) c.Assert(err, qt.IsNil) c.Check(cred, jimmtest.DBObjectEquals, dbmodel.CloudCredential{ - OwnerUsername: "alice@external", - Name: "cred-1", - CloudName: "test", - AuthType: "userpass", + OwnerIdentityName: "alice@external", + Name: "cred-1", + CloudName: "test", + AuthType: "userpass", Cloud: dbmodel.Cloud{ Name: "test", Type: "test-provider", diff --git a/internal/jimm/model.go b/internal/jimm/model.go index 46d9aaaf2..166384212 100644 --- a/internal/jimm/model.go +++ b/internal/jimm/model.go @@ -247,9 +247,9 @@ func (b *modelBuilder) WithCloudCredential(credentialTag names.CloudCredentialTa return b } credential := dbmodel.CloudCredential{ - Name: credentialTag.Name(), - CloudName: credentialTag.Cloud().Id(), - OwnerUsername: credentialTag.Owner().Id(), + Name: credentialTag.Name(), + CloudName: credentialTag.Cloud().Id(), + OwnerIdentityName: credentialTag.Owner().Id(), } err := b.jimm.Database.GetCloudCredential(b.ctx, &credential) if err != nil { diff --git a/internal/jimmtest/env.go b/internal/jimmtest/env.go index f71ab55e8..50f03c75e 100644 --- a/internal/jimmtest/env.go +++ b/internal/jimmtest/env.go @@ -355,7 +355,7 @@ func (cc *CloudCredential) DBObject(c *qt.C, db db.Database) dbmodel.CloudCreden cc.dbo.Cloud = cc.env.Cloud(cc.Cloud).DBObject(c, db) cc.dbo.CloudName = cc.dbo.Cloud.Name cc.dbo.Owner = cc.env.User(cc.Owner).DBObject(c, db) - cc.dbo.OwnerUsername = cc.dbo.Owner.Name + cc.dbo.OwnerIdentityName = cc.dbo.Owner.Name cc.dbo.AuthType = cc.AuthType cc.dbo.Attributes = cc.Attributes diff --git a/internal/jujuapi/access_control_test.go b/internal/jujuapi/access_control_test.go index e80ffae4c..97e53b78e 100644 --- a/internal/jujuapi/access_control_test.go +++ b/internal/jujuapi/access_control_test.go @@ -1529,10 +1529,10 @@ func createTestControllerEnvironment(ctx context.Context, c *gc.C, s *accessCont c.Assert(err, gc.IsNil) cred := dbmodel.CloudCredential{ - Name: petname.Generate(2, "-"), - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: petname.Generate(2, "-"), + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) c.Assert(err, gc.IsNil) diff --git a/local/seed_db/main.go b/local/seed_db/main.go index c8f0af37a..7dd82423b 100644 --- a/local/seed_db/main.go +++ b/local/seed_db/main.go @@ -84,10 +84,10 @@ func main() { } cred := dbmodel.CloudCredential{ - Name: petname.Generate(2, "-"), - CloudName: cloud.Name, - OwnerUsername: u.Name, - AuthType: "empty", + Name: petname.Generate(2, "-"), + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } if err = db.SetCloudCredential(ctx, &cred); err != nil { fmt.Println("failed to add cloud credential to db ", err) From 2303f29dc63e1d2bb78a2402f464acbec1a53d57 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Jan 2024 20:08:12 +0000 Subject: [PATCH 011/126] Rename `CloudDefaults.Username` to `IdentityName` Signed-off-by: Babak K. Shandiz --- internal/db/clouddefaults.go | 10 +-- internal/db/clouddefaults_test.go | 42 ++++++------ internal/dbmodel/clouddefaults.go | 4 +- internal/jimm/clouddefaults.go | 10 +-- internal/jimm/clouddefaults_test.go | 102 ++++++++++++++-------------- internal/jimm/model.go | 4 +- 6 files changed, 86 insertions(+), 86 deletions(-) diff --git a/internal/db/clouddefaults.go b/internal/db/clouddefaults.go index cba862111..116278fb1 100644 --- a/internal/db/clouddefaults.go +++ b/internal/db/clouddefaults.go @@ -20,8 +20,8 @@ func (d *Database) SetCloudDefaults(ctx context.Context, defaults *dbmodel.Cloud db := d.DB.WithContext(ctx) dbDefaults := dbmodel.CloudDefaults{ - Username: defaults.Username, - CloudID: defaults.CloudID, + IdentityName: defaults.IdentityName, + CloudID: defaults.CloudID, Cloud: dbmodel.Cloud{ Name: defaults.Cloud.Name, }, @@ -70,8 +70,8 @@ func (d *Database) UnsetCloudDefaults(ctx context.Context, defaults *dbmodel.Clo db := d.DB.WithContext(ctx) dbDefaults := dbmodel.CloudDefaults{ - Username: defaults.Username, - CloudID: defaults.CloudID, + IdentityName: defaults.IdentityName, + CloudID: defaults.CloudID, Cloud: dbmodel.Cloud{ Name: defaults.Cloud.Name, }, @@ -114,7 +114,7 @@ func (d *Database) CloudDefaults(ctx context.Context, defaults *dbmodel.CloudDef } db := d.DB.WithContext(ctx) - db = db.Where("username = ?", defaults.Username) + db = db.Where("username = ?", defaults.IdentityName) db = db.Joins("JOIN clouds ON clouds.id = cloud_defaults.cloud_id") if defaults.CloudID != 0 { db = db.Where("clouds.id = ?", defaults.CloudID) diff --git a/internal/db/clouddefaults_test.go b/internal/db/clouddefaults_test.go index 4fad49246..80a59b89e 100644 --- a/internal/db/clouddefaults_test.go +++ b/internal/db/clouddefaults_test.go @@ -48,11 +48,11 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { cloud := cloud1 cloud.Regions = nil defaults := dbmodel.CloudDefaults{ - Username: u.Name, - User: u, - CloudID: cloud.ID, - Cloud: cloud, - Region: cloud1.Regions[0].Name, + IdentityName: u.Name, + User: u, + CloudID: cloud.ID, + Cloud: cloud, + Region: cloud1.Regions[0].Name, Defaults: map[string]interface{}{ "key1": float64(17), "key2": "some other data", @@ -81,20 +81,20 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { }) dbDefaults := dbmodel.CloudDefaults{ - Username: u.Name, - CloudID: cloud2.ID, - Cloud: cloud2, - Region: cloud2.Regions[0].Name, + IdentityName: u.Name, + CloudID: cloud2.ID, + Cloud: cloud2, + Region: cloud2.Regions[0].Name, } err = s.Database.CloudDefaults(ctx, &dbDefaults) c.Assert(err, qt.ErrorMatches, "cloudregiondefaults not found") c.Assert(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) dbDefaults = dbmodel.CloudDefaults{ - Username: u.Name, - CloudID: cloud1.ID, - Cloud: cloud1, - Region: cloud1.Regions[0].Name, + IdentityName: u.Name, + CloudID: cloud1.ID, + Cloud: cloud1, + Region: cloud1.Regions[0].Name, } err = s.Database.CloudDefaults(ctx, &dbDefaults) c.Assert(err, qt.Equals, nil) @@ -106,20 +106,20 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { err = s.Database.CloudDefaults(ctx, &dbDefaults) c.Assert(err, qt.Equals, nil) c.Assert(dbDefaults, qt.CmpEquals(cmpopts.IgnoreTypes([]dbmodel.CloudRegion{}, gorm.Model{})), dbmodel.CloudDefaults{ - Username: u.Name, - User: u, - CloudID: cloud1.ID, - Cloud: cloud1, - Region: cloud1.Regions[0].Name, + IdentityName: u.Name, + User: u, + CloudID: cloud1.ID, + Cloud: cloud1, + Region: cloud1.Regions[0].Name, Defaults: map[string]interface{}{ "key3": "more data", }, }) err = s.Database.UnsetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: u.Name, - CloudID: cloud2.ID, - Region: "no-such-region", + IdentityName: u.Name, + CloudID: cloud2.ID, + Region: "no-such-region", }, []string{"key1", "key2", "unknown-key"}) c.Assert(err, qt.ErrorMatches, "cloudregiondefaults not found") } diff --git a/internal/dbmodel/clouddefaults.go b/internal/dbmodel/clouddefaults.go index 926f6bb90..38bfdd6fb 100644 --- a/internal/dbmodel/clouddefaults.go +++ b/internal/dbmodel/clouddefaults.go @@ -8,8 +8,8 @@ import "gorm.io/gorm" type CloudDefaults struct { gorm.Model - Username string - User Identity `gorm:"foreignKey:Username;references:Username"` + IdentityName string + User Identity `gorm:"foreignKey:IdentityName;references:Name"` CloudID uint Cloud Cloud diff --git a/internal/jimm/clouddefaults.go b/internal/jimm/clouddefaults.go index b654a6555..47c93cd51 100644 --- a/internal/jimm/clouddefaults.go +++ b/internal/jimm/clouddefaults.go @@ -54,10 +54,10 @@ func (j *JIMM) SetModelDefaults(ctx context.Context, user *dbmodel.Identity, clo } } err = j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Name, - CloudID: cloud.ID, - Region: region, - Defaults: configs, + IdentityName: user.Name, + CloudID: cloud.ID, + Region: region, + Defaults: configs, }) if err != nil { return errors.E(op, err) @@ -70,7 +70,7 @@ func (j *JIMM) UnsetModelDefaults(ctx context.Context, user *dbmodel.Identity, c const op = errors.Op("jimm.UnsetModelDefaults") defaults := dbmodel.CloudDefaults{ - Username: user.Name, + IdentityName: user.Name, Cloud: dbmodel.Cloud{ Name: cloudTag.Id(), }, diff --git a/internal/jimm/clouddefaults_test.go b/internal/jimm/clouddefaults_test.go index 02b996899..cbcec9fbe 100644 --- a/internal/jimm/clouddefaults_test.go +++ b/internal/jimm/clouddefaults_test.go @@ -62,12 +62,12 @@ func TestSetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ - Username: user.Name, - User: user, - CloudID: cloud.ID, - Cloud: cloud, - Region: "test-region", - Defaults: defaults, + IdentityName: user.Name, + User: user, + CloudID: cloud.ID, + Cloud: cloud, + Region: "test-region", + Defaults: defaults, } return testConfig{ @@ -102,11 +102,11 @@ func TestSetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ - Username: user.Name, - User: user, - CloudID: cloud.ID, - Cloud: cloud, - Defaults: defaults, + IdentityName: user.Name, + User: user, + CloudID: cloud.ID, + Cloud: cloud, + Defaults: defaults, } return testConfig{ @@ -135,11 +135,11 @@ func TestSetCloudDefaults(t *testing.T) { c.Assert(j.Database.DB.Create(&cloud).Error, qt.IsNil) j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Name, - User: user, - CloudID: cloud.ID, - Cloud: cloud, - Region: cloud.Regions[0].Name, + IdentityName: user.Name, + User: user, + CloudID: cloud.ID, + Cloud: cloud, + Region: cloud.Regions[0].Name, Defaults: map[string]interface{}{ "key1": float64(17), "key2": "a test string", @@ -154,12 +154,12 @@ func TestSetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ - Username: user.Name, - User: user, - CloudID: cloud.ID, - Cloud: cloud, - Region: "test-region", - Defaults: defaults, + IdentityName: user.Name, + User: user, + CloudID: cloud.ID, + Cloud: cloud, + Region: "test-region", + Defaults: defaults, } return testConfig{ @@ -248,7 +248,7 @@ func TestSetCloudDefaults(t *testing.T) { if testConfig.expectedError == "" { c.Assert(err, qt.Equals, nil) dbDefaults := dbmodel.CloudDefaults{ - Username: testConfig.expectedDefaults.Username, + IdentityName: testConfig.expectedDefaults.IdentityName, Cloud: dbmodel.Cloud{ Name: testConfig.expectedDefaults.Cloud.Name, }, @@ -301,9 +301,9 @@ func TestUnsetCloudDefaults(t *testing.T) { c.Assert(j.Database.DB.Create(&cloud).Error, qt.IsNil) err := j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Name, - CloudID: cloud.ID, - Region: cloud.Regions[0].Name, + IdentityName: user.Name, + CloudID: cloud.ID, + Region: cloud.Regions[0].Name, Defaults: map[string]interface{}{ "key1": float64(17), "key2": "a test string", @@ -320,11 +320,11 @@ func TestUnsetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ - Username: user.Name, - User: user, - CloudID: cloud.ID, - Cloud: cloud, - Region: "test-region", + IdentityName: user.Name, + User: user, + CloudID: cloud.ID, + Cloud: cloud, + Region: "test-region", Defaults: map[string]interface{}{ "key2": "a test string", }, @@ -356,8 +356,8 @@ func TestUnsetCloudDefaults(t *testing.T) { c.Assert(j.Database.DB.Create(&cloud).Error, qt.IsNil) err := j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Name, - CloudID: cloud.ID, + IdentityName: user.Name, + CloudID: cloud.ID, Defaults: map[string]interface{}{ "key1": float64(17), "key2": "a test string", @@ -374,11 +374,11 @@ func TestUnsetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ - Username: user.Name, - User: user, - CloudID: cloud.ID, - Cloud: cloud, - Region: "", + IdentityName: user.Name, + User: user, + CloudID: cloud.ID, + Cloud: cloud, + Region: "", Defaults: map[string]interface{}{ "key2": "a test string", }, @@ -441,7 +441,7 @@ func TestUnsetCloudDefaults(t *testing.T) { if testConfig.expectedError == "" { c.Assert(err, qt.Equals, nil) dbDefaults := dbmodel.CloudDefaults{ - Username: testConfig.expectedDefaults.Username, + IdentityName: testConfig.expectedDefaults.IdentityName, Cloud: dbmodel.Cloud{ Name: testConfig.cloud.Id(), }, @@ -502,9 +502,9 @@ func TestModelDefaultsForCloud(t *testing.T) { c.Assert(j.Database.DB.Create(&cloud2).Error, qt.IsNil) err = j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Name, - CloudID: cloud1.ID, - Region: cloud1.Regions[0].Name, + IdentityName: user.Name, + CloudID: cloud1.ID, + Region: cloud1.Regions[0].Name, Defaults: map[string]interface{}{ "key1": float64(17), "key2": "a test string", @@ -514,9 +514,9 @@ func TestModelDefaultsForCloud(t *testing.T) { c.Assert(err, qt.Equals, nil) err = j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Name, - CloudID: cloud1.ID, - Region: cloud1.Regions[1].Name, + IdentityName: user.Name, + CloudID: cloud1.ID, + Region: cloud1.Regions[1].Name, Defaults: map[string]interface{}{ "key2": "a different string", "key4": float64(42), @@ -525,9 +525,9 @@ func TestModelDefaultsForCloud(t *testing.T) { c.Assert(err, qt.Equals, nil) err = j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Name, - CloudID: cloud2.ID, - Region: cloud2.Regions[0].Name, + IdentityName: user.Name, + CloudID: cloud2.ID, + Region: cloud2.Regions[0].Name, Defaults: map[string]interface{}{ "key2": "a different string", "key4": float64(42), @@ -537,9 +537,9 @@ func TestModelDefaultsForCloud(t *testing.T) { c.Assert(err, qt.Equals, nil) err = j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ - Username: user.Name, - CloudID: cloud2.ID, - Region: "", + IdentityName: user.Name, + CloudID: cloud2.ID, + Region: "", Defaults: map[string]interface{}{ "key1": "value", "key4": float64(37), diff --git a/internal/jimm/model.go b/internal/jimm/model.go index 166384212..58873bf76 100644 --- a/internal/jimm/model.go +++ b/internal/jimm/model.go @@ -549,7 +549,7 @@ func (j *JIMM) AddModel(ctx context.Context, user *openfga.User, args *ModelCrea // fetch cloud defaults if args.Cloud != (names.CloudTag{}) { cloudDefaults := dbmodel.CloudDefaults{ - Username: user.Name, + IdentityName: user.Name, Cloud: dbmodel.Cloud{ Name: args.Cloud.Id(), }, @@ -585,7 +585,7 @@ func (j *JIMM) AddModel(ctx context.Context, user *openfga.User, args *ModelCrea // fetch cloud region defaults if args.Cloud != (names.CloudTag{}) && builder.cloudRegion != "" { cloudRegionDefaults := dbmodel.CloudDefaults{ - Username: user.Name, + IdentityName: user.Name, Cloud: dbmodel.Cloud{ Name: args.Cloud.Id(), }, From fa0456fe8f4a8ba5533650ca6755f979b72abfa1 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Jan 2024 20:10:07 +0000 Subject: [PATCH 012/126] Rename `Model.OwnerUsername` to `OwnerIdentityName` Signed-off-by: Babak K. Shandiz --- cmd/jimmctl/cmd/importmodel_test.go | 4 ++-- cmd/jimmctl/cmd/relation_test.go | 10 ++++---- internal/db/model.go | 4 ++-- internal/db/model_test.go | 18 +++++++------- internal/dbmodel/controller_test.go | 4 ++-- internal/dbmodel/model.go | 12 +++++----- internal/dbmodel/model_test.go | 28 +++++++++++----------- internal/jimm/applicationoffer.go | 6 ++--- internal/jimm/applicationoffer_test.go | 20 ++++++++-------- internal/jujuapi/access_control.go | 6 ++--- internal/jujuapi/access_control_test.go | 2 +- internal/jujuapi/applicationoffers_test.go | 2 +- local/seed_db/main.go | 2 +- 13 files changed, 59 insertions(+), 59 deletions(-) diff --git a/cmd/jimmctl/cmd/importmodel_test.go b/cmd/jimmctl/cmd/importmodel_test.go index 81222ab5d..fcea71caa 100644 --- a/cmd/jimmctl/cmd/importmodel_test.go +++ b/cmd/jimmctl/cmd/importmodel_test.go @@ -50,7 +50,7 @@ func (s *importModelSuite) TestImportModelSuperuser(c *gc.C) { model2.SetTag(names.NewModelTag(m.ModelUUID())) err = s.JIMM.Database.GetModel(context.Background(), &model2) c.Assert(err, gc.Equals, nil) - c.Check(model2.OwnerUsername, gc.Equals, "charlie@external") + c.Check(model2.OwnerIdentityName, gc.Equals, "charlie@external") } func (s *importModelSuite) TestImportModelFromLocalUser(c *gc.C) { @@ -78,7 +78,7 @@ func (s *importModelSuite) TestImportModelFromLocalUser(c *gc.C) { err = s.JIMM.Database.GetModel(context.Background(), &model2) c.Assert(err, gc.Equals, nil) c.Check(model2.CreatedAt.After(model.CreatedAt), gc.Equals, true) - c.Check(model2.OwnerUsername, gc.Equals, "alice@external") + c.Check(model2.OwnerIdentityName, gc.Equals, "alice@external") } func (s *importModelSuite) TestImportModelUnauthorized(c *gc.C) { diff --git a/cmd/jimmctl/cmd/relation_test.go b/cmd/jimmctl/cmd/relation_test.go index fc051764b..be79fe3a0 100644 --- a/cmd/jimmctl/cmd/relation_test.go +++ b/cmd/jimmctl/cmd/relation_test.go @@ -315,7 +315,7 @@ func initializeEnvironment(c *gc.C, ctx context.Context, db *db.Database, u dbmo String: "acdbf3e5-67e1-42a2-a2dc-64505265c030", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -327,7 +327,7 @@ func initializeEnvironment(c *gc.C, ctx context.Context, db *db.Database, u dbmo offer := dbmodel.ApplicationOffer{ ID: 1, UUID: "436b2264-d8f8-4e24-b16f-dd43c4116528", - URL: env.controllers[0].Name + ":" + env.models[0].OwnerUsername + "/" + env.models[0].Name + ".testoffer1", + URL: env.controllers[0].Name + ":" + env.models[0].OwnerIdentityName + "/" + env.models[0].Name + ".testoffer1", Name: "testoffer1", ModelID: model.ID, Model: model, @@ -371,11 +371,11 @@ func (s *relationSuite) TestListRelations(c *gc.C) { }, { Object: "group-group-1#member", Relation: "administrator", - TargetObject: "model-" + env.controllers[0].Name + ":" + env.models[0].OwnerUsername + "/" + env.models[0].Name, + TargetObject: "model-" + env.controllers[0].Name + ":" + env.models[0].OwnerIdentityName + "/" + env.models[0].Name, }, { Object: "user-" + env.users[1].Name, Relation: "administrator", - TargetObject: "applicationoffer-" + env.controllers[0].Name + ":" + env.applicationOffers[0].Model.OwnerUsername + "/" + env.applicationOffers[0].Model.Name + "." + env.applicationOffers[0].Name, + TargetObject: "applicationoffer-" + env.controllers[0].Name + ":" + env.applicationOffers[0].Model.OwnerIdentityName + "/" + env.applicationOffers[0].Model.Name + "." + env.applicationOffers[0].Name, }} for _, relation := range relations { _, err := cmdtesting.RunCommand(c, cmd.NewAddRelationCommandForTesting(s.ClientStore(), bClient), relation.Object, relation.Relation, relation.TargetObject) @@ -490,7 +490,7 @@ func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { String: id.String(), Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, diff --git a/internal/db/model.go b/internal/db/model.go index 4b22cd493..cc4b737f9 100644 --- a/internal/db/model.go +++ b/internal/db/model.go @@ -42,8 +42,8 @@ func (d *Database) GetModel(ctx context.Context, model *dbmodel.Model) error { } } else if model.ID != 0 { db = db.Where("id = ?", model.ID) - } else if model.OwnerUsername != "" && model.Name != "" { - db = db.Where("owner_username = ? AND name = ?", model.OwnerUsername, model.Name) + } else if model.OwnerIdentityName != "" && model.Name != "" { + db = db.Where("owner_username = ? AND name = ?", model.OwnerIdentityName, model.Name) } else if model.ControllerID != 0 { // TODO(ales): fix ordering of where fields and handle error to represent what is *actually* required. db = db.Where("controller_id = ?", model.ControllerID) diff --git a/internal/db/model_test.go b/internal/db/model_test.go index 599d65e96..62b5a60e4 100644 --- a/internal/db/model_test.go +++ b/internal/db/model_test.go @@ -69,7 +69,7 @@ func (s *dbSuite) TestAddModel(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -142,7 +142,7 @@ func (s *dbSuite) TestGetModel(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, Owner: u, ControllerID: controller.ID, Controller: controller, @@ -189,8 +189,8 @@ func (s *dbSuite) TestGetModel(c *qt.C) { c.Assert(eError.Code, qt.Equals, errors.CodeNotFound) dbModel = dbmodel.Model{ - Name: model.Name, - OwnerUsername: model.OwnerUsername, + Name: model.Name, + OwnerIdentityName: model.OwnerIdentityName, } err = s.Database.GetModel(context.Background(), &dbModel) c.Assert(err, qt.IsNil) @@ -238,7 +238,7 @@ func (s *dbSuite) TestUpdateModel(c *qt.C) { model := dbmodel.Model{ Name: "test-model-1", - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -316,7 +316,7 @@ func (s *dbSuite) TestDeleteModel(c *qt.C) { model := dbmodel.Model{ Name: "test-model-1", - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, Controller: controller, CloudRegionID: cloud.Regions[0].ID, @@ -401,7 +401,7 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred1.ID, @@ -425,7 +425,7 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000002", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred2.ID, @@ -454,7 +454,7 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, Controller: controller, CloudRegionID: cloud.Regions[0].ID, diff --git a/internal/dbmodel/controller_test.go b/internal/dbmodel/controller_test.go index efd8ff0dc..068668a82 100644 --- a/internal/dbmodel/controller_test.go +++ b/internal/dbmodel/controller_test.go @@ -119,7 +119,7 @@ func TestControllerModels(t *testing.T) { UpdatedAt: m1.UpdatedAt, Name: m1.Name, UUID: m1.UUID, - OwnerUsername: m1.OwnerUsername, + OwnerIdentityName: m1.OwnerIdentityName, ControllerID: m1.ControllerID, CloudRegionID: m1.CloudRegionID, CloudCredentialID: m1.CloudCredentialID, @@ -129,7 +129,7 @@ func TestControllerModels(t *testing.T) { UpdatedAt: m2.UpdatedAt, Name: m2.Name, UUID: m2.UUID, - OwnerUsername: m2.OwnerUsername, + OwnerIdentityName: m2.OwnerIdentityName, ControllerID: m2.ControllerID, CloudRegionID: m2.CloudRegionID, CloudCredentialID: m2.CloudCredentialID, diff --git a/internal/dbmodel/model.go b/internal/dbmodel/model.go index 4a3fa3944..740810dc1 100644 --- a/internal/dbmodel/model.go +++ b/internal/dbmodel/model.go @@ -29,9 +29,9 @@ type Model struct { // UUID is the UUID of the model. UUID sql.NullString - // Owner is user that owns the model. - OwnerUsername string - Owner Identity `gorm:"foreignkey:OwnerUsername;references:Username"` + // Owner is identity that owns the model. + OwnerIdentityName string + Owner Identity `gorm:"foreignkey:OwnerIdentityName;references:Name"` // Controller is the controller that is hosting the model. ControllerID uint @@ -104,7 +104,7 @@ func (m *Model) SetTag(t names.ModelTag) { // FromModelUpdate updates the model from the given ModelUpdate. func (m *Model) SwitchOwner(u *Identity) { - m.OwnerUsername = u.Name + m.OwnerIdentityName = u.Name m.Owner = *u } @@ -120,7 +120,7 @@ func (m *Model) FromJujuModelInfo(info jujuparams.ModelInfo) error { if err != nil { return errors.E(err) } - m.OwnerUsername = ut.Id() + m.OwnerIdentityName = ut.Id() } m.Life = string(info.Life) m.Status.FromJujuEntityStatus(info.Status) @@ -167,7 +167,7 @@ func (m Model) ToJujuModel() jujuparams.Model { jm.Name = m.Name jm.UUID = m.UUID.String jm.Type = m.Type - jm.OwnerTag = names.NewUserTag(m.OwnerUsername).String() + jm.OwnerTag = names.NewUserTag(m.OwnerIdentityName).String() return jm } diff --git a/internal/dbmodel/model_test.go b/internal/dbmodel/model_test.go index 9b3f1632c..60ba5080c 100644 --- a/internal/dbmodel/model_test.go +++ b/internal/dbmodel/model_test.go @@ -214,15 +214,15 @@ func TestToJujuModel(t *testing.T) { String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - OwnerUsername: u.Name, - Owner: u, - Controller: ctl, - CloudRegion: cl.Regions[0], - CloudCredential: cred, - Type: "iaas", - IsController: false, - DefaultSeries: "warty", - Life: constants.ALIVE.String(), + OwnerIdentityName: u.Name, + Owner: u, + Controller: ctl, + CloudRegion: cl.Regions[0], + CloudCredential: cred, + Type: "iaas", + IsController: false, + DefaultSeries: "warty", + Life: constants.ALIVE.String(), Status: dbmodel.Status{ Status: "available", Since: sql.NullTime{ @@ -418,11 +418,11 @@ func TestModelFromJujuModelInfo(t *testing.T) { Name: "bob@external", }, }, - OwnerUsername: "bob@external", - Type: "iaas", - IsController: false, - DefaultSeries: "warty", - Life: constants.ALIVE.String(), + OwnerIdentityName: "bob@external", + Type: "iaas", + IsController: false, + DefaultSeries: "warty", + Life: constants.ALIVE.String(), Status: dbmodel.Status{ Status: "available", Since: sql.NullTime{ diff --git a/internal/jimm/applicationoffer.go b/internal/jimm/applicationoffer.go index cd201f6ab..c1ebfd1bf 100644 --- a/internal/jimm/applicationoffer.go +++ b/internal/jimm/applicationoffer.go @@ -61,7 +61,7 @@ func (j *JIMM) Offer(ctx context.Context, user *openfga.User, offer AddApplicati } offerURL := crossmodel.OfferURL{ - User: model.OwnerUsername, + User: model.OwnerIdentityName, ModelName: model.Name, // Confusingly the application name in the offer URL is // actually the offer name. @@ -726,8 +726,8 @@ func (j *JIMM) ListApplicationOffers(ctx context.Context, user *openfga.User, fi for _, k := range keys { m := dbmodel.Model{ - Name: k.name, - OwnerUsername: k.ownerUsername, + Name: k.name, + OwnerIdentityName: k.ownerUsername, } offerDetails, err := j.listApplicationOffersForModel(ctx, user, &m, modelFilters[k]) if err != nil { diff --git a/internal/jimm/applicationoffer_test.go b/internal/jimm/applicationoffer_test.go index 02bb5019b..8707cea1f 100644 --- a/internal/jimm/applicationoffer_test.go +++ b/internal/jimm/applicationoffer_test.go @@ -137,7 +137,7 @@ var initializeEnvironment = func(c *qt.C, ctx context.Context, db *db.Database, String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -626,7 +626,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -994,7 +994,7 @@ func TestGetApplicationOffer(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1290,7 +1290,7 @@ func TestOffer(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1412,7 +1412,7 @@ func TestOffer(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1533,7 +1533,7 @@ func TestOffer(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1627,7 +1627,7 @@ func TestOffer(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1715,7 +1715,7 @@ func TestOffer(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1803,7 +1803,7 @@ func TestOffer(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -1933,7 +1933,7 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { String: "00000000-0000-0000-0000-0000-0000000000003", Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, diff --git a/internal/jujuapi/access_control.go b/internal/jujuapi/access_control.go index aecccdcdc..85117f604 100644 --- a/internal/jujuapi/access_control.go +++ b/internal/jujuapi/access_control.go @@ -251,7 +251,7 @@ func resolveTag(jimmUUID string, db *db.Database, tag string) (*ofganames.Tag, e return nil, errors.E("controller not found") } model.ControllerID = controller.ID - model.OwnerUsername = userName + model.OwnerIdentityName = userName model.Name = modelName } @@ -469,7 +469,7 @@ func (r *controllerRoot) toJAASTag(ctx context.Context, tag *ofganames.Tag) (str if err != nil { return "", errors.E(err, "failed to fetch model information") } - modelString := names.ModelTagKind + "-" + model.Controller.Name + ":" + model.OwnerUsername + "/" + model.Name + modelString := names.ModelTagKind + "-" + model.Controller.Name + ":" + model.OwnerIdentityName + "/" + model.Name if tag.Relation.String() != "" { modelString = modelString + "#" + tag.Relation.String() } @@ -482,7 +482,7 @@ func (r *controllerRoot) toJAASTag(ctx context.Context, tag *ofganames.Tag) (str if err != nil { return "", errors.E(err, "failed to fetch application offer information") } - aoString := names.ApplicationOfferTagKind + "-" + ao.Model.Controller.Name + ":" + ao.Model.OwnerUsername + "/" + ao.Model.Name + "." + ao.Name + aoString := names.ApplicationOfferTagKind + "-" + ao.Model.Controller.Name + ":" + ao.Model.OwnerIdentityName + "/" + ao.Model.Name + "." + ao.Name if tag.Relation.String() != "" { aoString = aoString + "#" + tag.Relation.String() } diff --git a/internal/jujuapi/access_control_test.go b/internal/jujuapi/access_control_test.go index 97e53b78e..f29309f2e 100644 --- a/internal/jujuapi/access_control_test.go +++ b/internal/jujuapi/access_control_test.go @@ -1543,7 +1543,7 @@ func createTestControllerEnvironment(ctx context.Context, c *gc.C, s *accessCont String: id.String(), Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, diff --git a/internal/jujuapi/applicationoffers_test.go b/internal/jujuapi/applicationoffers_test.go index f57bc843c..01ad65bb9 100644 --- a/internal/jujuapi/applicationoffers_test.go +++ b/internal/jujuapi/applicationoffers_test.go @@ -452,7 +452,7 @@ func (s *applicationOffersSuite) TestFindApplicationOffers(c *gc.C) { client2 := applicationoffers.NewClient(conn2) offers, err = client2.FindApplicationOffers(crossmodel.ApplicationOfferFilter{ - OwnerName: s.Model.OwnerUsername, + OwnerName: s.Model.OwnerIdentityName, ModelName: s.Model.Name, ApplicationName: "test-app", OfferName: "test-offer1", diff --git a/local/seed_db/main.go b/local/seed_db/main.go index 7dd82423b..b3f16703f 100644 --- a/local/seed_db/main.go +++ b/local/seed_db/main.go @@ -100,7 +100,7 @@ func main() { String: id.String(), Valid: true, }, - OwnerUsername: u.Name, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, From 53c820aa4872c8560523d522022ec7d6c3bcf355 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Jan 2024 20:13:08 +0000 Subject: [PATCH 013/126] Rename `ApplicationOfferConnection.Username` to `IdentityName` Signed-off-by: Babak K. Shandiz --- internal/dbmodel/applicationoffer.go | 6 +++--- internal/jimm/applicationoffer_test.go | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/dbmodel/applicationoffer.go b/internal/dbmodel/applicationoffer.go index 2c2e6d8f2..419d5901b 100644 --- a/internal/dbmodel/applicationoffer.go +++ b/internal/dbmodel/applicationoffer.go @@ -104,7 +104,7 @@ func (o *ApplicationOffer) FromJujuApplicationOfferAdminDetails(offerDetails juj o.Connections[i] = ApplicationOfferConnection{ SourceModelTag: connection.SourceModelTag, RelationID: connection.RelationId, - Username: connection.Username, + IdentityName: connection.Username, Endpoint: connection.Endpoint, IngressSubnets: connection.IngressSubnets, } @@ -137,7 +137,7 @@ func (o *ApplicationOffer) ToJujuApplicationOfferDetails() jujuparams.Applicatio connections[i] = jujuparams.OfferConnection{ SourceModelTag: connection.SourceModelTag, RelationId: connection.RelationID, - Username: connection.Username, + Username: connection.IdentityName, Endpoint: connection.Endpoint, IngressSubnets: connection.IngressSubnets, } @@ -199,7 +199,7 @@ type ApplicationOfferConnection struct { SourceModelTag string RelationID int - Username string + IdentityName string Endpoint string IngressSubnets Strings } diff --git a/internal/jimm/applicationoffer_test.go b/internal/jimm/applicationoffer_test.go index 8707cea1f..ee481a3b4 100644 --- a/internal/jimm/applicationoffer_test.go +++ b/internal/jimm/applicationoffer_test.go @@ -1036,7 +1036,7 @@ func TestGetApplicationOffer(t *testing.T) { ApplicationOfferID: 1, SourceModelTag: "test-model-src", RelationID: 1, - Username: "unknown", + IdentityName: "unknown", Endpoint: "test-endpoint", }}, } @@ -1345,7 +1345,7 @@ func TestOffer(t *testing.T) { ApplicationOfferID: 1, SourceModelTag: "test-model-src", RelationID: 1, - Username: "unknown", + IdentityName: "unknown", Endpoint: "test-endpoint", }}, } @@ -1988,7 +1988,7 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { ApplicationOfferID: 1, SourceModelTag: "test-model-src", RelationID: 1, - Username: "unknown", + IdentityName: "unknown", Endpoint: "test-endpoint", }}, } @@ -2334,7 +2334,7 @@ func TestUpdateOffer(t *testing.T) { ApplicationOfferID: 1, SourceModelTag: "test-model-src", RelationID: 1, - Username: "unknown", + IdentityName: "unknown", Endpoint: "test-endpoint", }}, Endpoints: []dbmodel.ApplicationOfferRemoteEndpoint{{ From 9e008380f0f75eeabf4e010d30521fc6aa39db20 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Jan 2024 20:14:34 +0000 Subject: [PATCH 014/126] Rename `UserModelDefaults.Username` to `IdentityName` Signed-off-by: Babak K. Shandiz --- internal/db/usermodeldefaults.go | 4 ++-- internal/db/usermodeldefaults_test.go | 18 +++++++++--------- internal/dbmodel/usermodeldefaults.go | 4 ++-- internal/jimm/usermodeldefaults.go | 6 +++--- internal/jimm/usermodeldefaults_test.go | 22 +++++++++++----------- internal/jimmtest/env.go | 2 +- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/internal/db/usermodeldefaults.go b/internal/db/usermodeldefaults.go index 67fd81e73..9b2de0005 100644 --- a/internal/db/usermodeldefaults.go +++ b/internal/db/usermodeldefaults.go @@ -19,7 +19,7 @@ func (d *Database) SetUserModelDefaults(ctx context.Context, defaults *dbmodel.U db := d.DB.WithContext(ctx) dbDefaults := dbmodel.UserModelDefaults{ - Username: defaults.Username, + IdentityName: defaults.IdentityName, } // try to fetch cloud defaults from the db err := d.UserModelDefaults(ctx, &dbDefaults) @@ -63,7 +63,7 @@ func (d *Database) UserModelDefaults(ctx context.Context, defaults *dbmodel.User } db := d.DB.WithContext(ctx) - db = db.Where("username = ?", defaults.Username) + db = db.Where("username = ?", defaults.IdentityName) result := db.Preload("User").First(&defaults) if result.Error != nil { diff --git a/internal/db/usermodeldefaults_test.go b/internal/db/usermodeldefaults_test.go index bb88b0286..d26eac4e9 100644 --- a/internal/db/usermodeldefaults_test.go +++ b/internal/db/usermodeldefaults_test.go @@ -48,9 +48,9 @@ func TestSetUserModelDefaults(t *testing.T) { } expectedDefaults := dbmodel.UserModelDefaults{ - Username: user.Name, - User: user, - Defaults: defaults, + IdentityName: user.Name, + Identity: user, + Defaults: defaults, } return testConfig{ @@ -68,8 +68,8 @@ func TestSetUserModelDefaults(t *testing.T) { c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) j.Database.SetUserModelDefaults(ctx, &dbmodel.UserModelDefaults{ - Username: user.Name, - User: user, + IdentityName: user.Name, + Identity: user, Defaults: map[string]interface{}{ "key1": float64(17), "key2": "a test string", @@ -83,9 +83,9 @@ func TestSetUserModelDefaults(t *testing.T) { } expectedDefaults := dbmodel.UserModelDefaults{ - Username: user.Name, - User: user, - Defaults: defaults, + IdentityName: user.Name, + Identity: user, + Defaults: defaults, } return testConfig{ @@ -151,7 +151,7 @@ func TestSetUserModelDefaults(t *testing.T) { if testConfig.expectedError == "" { c.Assert(err, qt.Equals, nil) dbDefaults := dbmodel.UserModelDefaults{ - Username: testConfig.expectedDefaults.Username, + IdentityName: testConfig.expectedDefaults.IdentityName, } err = j.Database.UserModelDefaults(ctx, &dbDefaults) c.Assert(err, qt.Equals, nil) diff --git a/internal/dbmodel/usermodeldefaults.go b/internal/dbmodel/usermodeldefaults.go index 28c9743e9..dc55ef2ac 100644 --- a/internal/dbmodel/usermodeldefaults.go +++ b/internal/dbmodel/usermodeldefaults.go @@ -8,8 +8,8 @@ import "gorm.io/gorm" type UserModelDefaults struct { gorm.Model - Username string - User Identity `gorm:"foreignKey:Username;references:Username"` + IdentityName string + Identity Identity `gorm:"foreignKey:IdentityName;references:Name"` Defaults Map } diff --git a/internal/jimm/usermodeldefaults.go b/internal/jimm/usermodeldefaults.go index 2975f622f..3540cb2a6 100644 --- a/internal/jimm/usermodeldefaults.go +++ b/internal/jimm/usermodeldefaults.go @@ -20,8 +20,8 @@ func (j *JIMM) SetUserModelDefaults(ctx context.Context, user *dbmodel.Identity, } err := j.Database.SetUserModelDefaults(ctx, &dbmodel.UserModelDefaults{ - Username: user.Name, - Defaults: configs, + IdentityName: user.Name, + Defaults: configs, }) if err != nil { return errors.E(op, err) @@ -34,7 +34,7 @@ func (j *JIMM) UserModelDefaults(ctx context.Context, user *dbmodel.Identity) (m const op = errors.Op("jimm.UserModelDefaults") defaults := dbmodel.UserModelDefaults{ - Username: user.Name, + IdentityName: user.Name, } err := j.Database.UserModelDefaults(ctx, &defaults) if err != nil { diff --git a/internal/jimm/usermodeldefaults_test.go b/internal/jimm/usermodeldefaults_test.go index d0a0746ae..737a4d471 100644 --- a/internal/jimm/usermodeldefaults_test.go +++ b/internal/jimm/usermodeldefaults_test.go @@ -48,9 +48,9 @@ func TestSetUserModelDefaults(t *testing.T) { } expectedDefaults := dbmodel.UserModelDefaults{ - Username: user.Name, - User: user, - Defaults: defaults, + IdentityName: user.Name, + Identity: user, + Defaults: defaults, } return testConfig{ @@ -68,8 +68,8 @@ func TestSetUserModelDefaults(t *testing.T) { c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) j.Database.SetUserModelDefaults(ctx, &dbmodel.UserModelDefaults{ - Username: user.Name, - User: user, + IdentityName: user.Name, + Identity: user, Defaults: map[string]interface{}{ "key1": float64(17), "key2": "a test string", @@ -83,9 +83,9 @@ func TestSetUserModelDefaults(t *testing.T) { } expectedDefaults := dbmodel.UserModelDefaults{ - Username: user.Name, - User: user, - Defaults: defaults, + IdentityName: user.Name, + Identity: user, + Defaults: defaults, } return testConfig{ @@ -132,7 +132,7 @@ func TestSetUserModelDefaults(t *testing.T) { if testConfig.expectedError == "" { c.Assert(err, qt.Equals, nil) dbDefaults := dbmodel.UserModelDefaults{ - Username: testConfig.expectedDefaults.Username, + IdentityName: testConfig.expectedDefaults.IdentityName, } err = j.Database.UserModelDefaults(ctx, &dbDefaults) c.Assert(err, qt.Equals, nil) @@ -182,8 +182,8 @@ func TestUserModelDefaults(t *testing.T) { c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) j.Database.SetUserModelDefaults(ctx, &dbmodel.UserModelDefaults{ - Username: user.Name, - User: user, + IdentityName: user.Name, + Identity: user, Defaults: map[string]interface{}{ "key1": float64(42), "key2": "a changed string", diff --git a/internal/jimmtest/env.go b/internal/jimmtest/env.go index 50f03c75e..7c305d817 100644 --- a/internal/jimmtest/env.go +++ b/internal/jimmtest/env.go @@ -261,7 +261,7 @@ func (cd *UserDefaults) DBObject(c *qt.C, db db.Database) dbmodel.UserModelDefau return cd.dbo } - cd.dbo.User = cd.env.User(cd.User).DBObject(c, db) + cd.dbo.Identity = cd.env.User(cd.User).DBObject(c, db) cd.dbo.Defaults = cd.Defaults err := db.SetUserModelDefaults(context.Background(), &cd.dbo) From ca49edfc17709b737d95a1b6316b584964b24200 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Jan 2024 20:18:07 +0000 Subject: [PATCH 015/126] Undo renaming `controllers.admin_user` and `audit_log.user_tag` columns Signed-off-by: Babak K. Shandiz --- internal/dbmodel/sql/postgres/1_6.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql index fe8161880..d001b32b5 100644 --- a/internal/dbmodel/sql/postgres/1_6.sql +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -15,8 +15,8 @@ ALTER TABLE IF EXISTS application_offer_connections RENAME COLUMN username TO id ALTER TABLE IF EXISTS user_model_defaults RENAME COLUMN username TO identity_name; -- TODO (babakks): Do we need to rename these two instances as well? -ALTER TABLE IF EXISTS controllers RENAME COLUMN admin_user TO admin_identity_name; -ALTER TABLE IF EXISTS audit_log RENAME COLUMN user_tag TO identity_tag; +-- - ALTER TABLE IF EXISTS controllers RENAME COLUMN admin_user TO admin_identity_name; +-- - ALTER TABLE IF EXISTS audit_log RENAME COLUMN user_tag TO identity_tag; -- We don't need to rename columns in these tables, because they're already -- dropped in an earlier migration: From b721e12f5bf9c2668dd33e298a3c8c9073a26ecf Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Jan 2024 20:21:01 +0000 Subject: [PATCH 016/126] Update comment with Jira card ID Signed-off-by: Babak K. Shandiz --- internal/dbmodel/sql/postgres/1_6.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql index d001b32b5..a4cd033b8 100644 --- a/internal/dbmodel/sql/postgres/1_6.sql +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -14,7 +14,7 @@ ALTER TABLE IF EXISTS models RENAME COLUMN owner_username TO owner_identity_name ALTER TABLE IF EXISTS application_offer_connections RENAME COLUMN username TO identity_name; ALTER TABLE IF EXISTS user_model_defaults RENAME COLUMN username TO identity_name; --- TODO (babakks): Do we need to rename these two instances as well? +-- TODO (CSS-6701): Do we need to rename these two instances as well? -- - ALTER TABLE IF EXISTS controllers RENAME COLUMN admin_user TO admin_identity_name; -- - ALTER TABLE IF EXISTS audit_log RENAME COLUMN user_tag TO identity_tag; From bfcfa388f254dcf935d681e6b77746bfa281804e Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Jan 2024 20:24:03 +0000 Subject: [PATCH 017/126] Rename `CloudDefaults.User` to `Identity` Signed-off-by: Babak K. Shandiz --- internal/db/clouddefaults_test.go | 4 ++-- internal/dbmodel/clouddefaults.go | 2 +- internal/jimm/clouddefaults_test.go | 12 ++++++------ internal/jimmtest/env.go | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/db/clouddefaults_test.go b/internal/db/clouddefaults_test.go index 80a59b89e..dfbbb9b2c 100644 --- a/internal/db/clouddefaults_test.go +++ b/internal/db/clouddefaults_test.go @@ -49,7 +49,7 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { cloud.Regions = nil defaults := dbmodel.CloudDefaults{ IdentityName: u.Name, - User: u, + Identity: u, CloudID: cloud.ID, Cloud: cloud, Region: cloud1.Regions[0].Name, @@ -107,7 +107,7 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { c.Assert(err, qt.Equals, nil) c.Assert(dbDefaults, qt.CmpEquals(cmpopts.IgnoreTypes([]dbmodel.CloudRegion{}, gorm.Model{})), dbmodel.CloudDefaults{ IdentityName: u.Name, - User: u, + Identity: u, CloudID: cloud1.ID, Cloud: cloud1, Region: cloud1.Regions[0].Name, diff --git a/internal/dbmodel/clouddefaults.go b/internal/dbmodel/clouddefaults.go index 38bfdd6fb..3c52814b9 100644 --- a/internal/dbmodel/clouddefaults.go +++ b/internal/dbmodel/clouddefaults.go @@ -9,7 +9,7 @@ type CloudDefaults struct { gorm.Model IdentityName string - User Identity `gorm:"foreignKey:IdentityName;references:Name"` + Identity Identity `gorm:"foreignKey:IdentityName;references:Name"` CloudID uint Cloud Cloud diff --git a/internal/jimm/clouddefaults_test.go b/internal/jimm/clouddefaults_test.go index cbcec9fbe..56b131a88 100644 --- a/internal/jimm/clouddefaults_test.go +++ b/internal/jimm/clouddefaults_test.go @@ -63,7 +63,7 @@ func TestSetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ IdentityName: user.Name, - User: user, + Identity: user, CloudID: cloud.ID, Cloud: cloud, Region: "test-region", @@ -103,7 +103,7 @@ func TestSetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ IdentityName: user.Name, - User: user, + Identity: user, CloudID: cloud.ID, Cloud: cloud, Defaults: defaults, @@ -136,7 +136,7 @@ func TestSetCloudDefaults(t *testing.T) { j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ IdentityName: user.Name, - User: user, + Identity: user, CloudID: cloud.ID, Cloud: cloud, Region: cloud.Regions[0].Name, @@ -155,7 +155,7 @@ func TestSetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ IdentityName: user.Name, - User: user, + Identity: user, CloudID: cloud.ID, Cloud: cloud, Region: "test-region", @@ -321,7 +321,7 @@ func TestUnsetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ IdentityName: user.Name, - User: user, + Identity: user, CloudID: cloud.ID, Cloud: cloud, Region: "test-region", @@ -375,7 +375,7 @@ func TestUnsetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ IdentityName: user.Name, - User: user, + Identity: user, CloudID: cloud.ID, Cloud: cloud, Region: "", diff --git a/internal/jimmtest/env.go b/internal/jimmtest/env.go index 7c305d817..732004362 100644 --- a/internal/jimmtest/env.go +++ b/internal/jimmtest/env.go @@ -285,7 +285,7 @@ func (cd *CloudDefaults) DBObject(c *qt.C, db db.Database) dbmodel.CloudDefaults return cd.dbo } - cd.dbo.User = cd.env.User(cd.User).DBObject(c, db) + cd.dbo.Identity = cd.env.User(cd.User).DBObject(c, db) cd.dbo.Cloud = cd.env.Cloud(cd.Cloud).DBObject(c, db) cd.dbo.Region = cd.Region cd.dbo.Defaults = cd.Defaults From d2ed00ba0b3758a943cc75863c09795e9b25d623 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 12 Jan 2024 12:41:54 +0000 Subject: [PATCH 018/126] Update Gorm magic strings Signed-off-by: Babak K. Shandiz --- internal/db/cloudcredential.go | 10 +++++----- internal/db/cloudcredential_test.go | 2 +- internal/db/clouddefaults.go | 8 ++++---- internal/db/model.go | 2 +- internal/db/user.go | 8 ++++---- internal/db/user_test.go | 4 ++-- internal/db/usermodeldefaults.go | 4 ++-- internal/dbmodel/user_test.go | 8 ++++---- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/internal/db/cloudcredential.go b/internal/db/cloudcredential.go index b48ce8997..6b377879b 100644 --- a/internal/db/cloudcredential.go +++ b/internal/db/cloudcredential.go @@ -50,7 +50,7 @@ func (d *Database) GetCloudCredential(ctx context.Context, cred *dbmodel.CloudCr db := d.DB.WithContext(ctx) db = db.Preload("Cloud") db = db.Preload("Models") - if err := db.Where("cloud_name = ? AND owner_username = ? AND name = ?", cred.CloudName, cred.OwnerIdentityName, cred.Name).First(&cred).Error; err != nil { + if err := db.Where("cloud_name = ? AND owner_identity_name = ? AND name = ?", cred.CloudName, cred.OwnerIdentityName, cred.Name).First(&cred).Error; err != nil { err := dbError(err) if errors.ErrorCode(err) == errors.CodeNotFound { return errors.E(op, errors.CodeNotFound, fmt.Sprintf("cloudcredential %q not found", cred.CloudName+"/"+cred.OwnerIdentityName+"/"+cred.Name), err) @@ -61,10 +61,10 @@ func (d *Database) GetCloudCredential(ctx context.Context, cred *dbmodel.CloudCr } // ForEachCloudCredential iterates through all cloud credentials owned by -// the given user calling the given function with each one. If cloud is +// the given identity calling the given function with each one. If cloud is // specified then the cloud-credentials are filtered to only return // credentials for that cloud. -func (d *Database) ForEachCloudCredential(ctx context.Context, username, cloud string, f func(*dbmodel.CloudCredential) error) error { +func (d *Database) ForEachCloudCredential(ctx context.Context, identityName, cloud string, f func(*dbmodel.CloudCredential) error) error { const op = errors.Op("db.ForEachCloudCredential") if err := d.ready(); err != nil { @@ -74,9 +74,9 @@ func (d *Database) ForEachCloudCredential(ctx context.Context, username, cloud s db := d.DB.WithContext(ctx) mdb := db.Model(dbmodel.CloudCredential{}) if cloud == "" { - mdb = mdb.Where("owner_username = ?", username) + mdb = mdb.Where("owner_identity_name = ?", identityName) } else { - mdb = mdb.Where("cloud_name = ? AND owner_username = ?", cloud, username) + mdb = mdb.Where("cloud_name = ? AND owner_identity_name = ?", cloud, identityName) } rows, err := mdb.Rows() if err != nil { diff --git a/internal/db/cloudcredential_test.go b/internal/db/cloudcredential_test.go index 108ed1f67..0ad8f039e 100644 --- a/internal/db/cloudcredential_test.go +++ b/internal/db/cloudcredential_test.go @@ -83,7 +83,7 @@ func (s *dbSuite) TestSetCloudCredential(c *qt.C) { c.Assert(err, qt.Equals, nil) var dbCred dbmodel.CloudCredential - result := s.Database.DB.Where("cloud_name = ? AND owner_username = ? AND name = ?", cloud.Name, u.Name, cred.Name).First(&dbCred) + result := s.Database.DB.Where("cloud_name = ? AND owner_identity_name = ? AND name = ?", cloud.Name, u.Name, cred.Name).First(&dbCred) c.Assert(result.Error, qt.Equals, nil) c.Assert(dbCred, qt.DeepEquals, cred) diff --git a/internal/db/clouddefaults.go b/internal/db/clouddefaults.go index 116278fb1..8a2ade89a 100644 --- a/internal/db/clouddefaults.go +++ b/internal/db/clouddefaults.go @@ -114,7 +114,7 @@ func (d *Database) CloudDefaults(ctx context.Context, defaults *dbmodel.CloudDef } db := d.DB.WithContext(ctx) - db = db.Where("username = ?", defaults.IdentityName) + db = db.Where("identity_name = ?", defaults.IdentityName) db = db.Joins("JOIN clouds ON clouds.id = cloud_defaults.cloud_id") if defaults.CloudID != 0 { db = db.Where("clouds.id = ?", defaults.CloudID) @@ -123,7 +123,7 @@ func (d *Database) CloudDefaults(ctx context.Context, defaults *dbmodel.CloudDef } db = db.Where("region = ?", defaults.Region) - result := db.Preload("User").Preload("Cloud").First(&defaults) + result := db.Preload("Identity").Preload("Cloud").First(&defaults) if result.Error != nil { err := dbError(result.Error) if errors.ErrorCode(err) == errors.CodeNotFound { @@ -143,12 +143,12 @@ func (d *Database) ModelDefaultsForCloud(ctx context.Context, user *dbmodel.Iden } db := d.DB.WithContext(ctx) - db = db.Where("username = ?", user.Name) + db = db.Where("identity_name = ?", user.Name) db = db.Joins("JOIN clouds ON clouds.id = cloud_defaults.cloud_id") db = db.Where("clouds.name = ?", cloud.Id()) var defaults []dbmodel.CloudDefaults - result := db.Preload("User").Preload("Cloud").Find(&defaults) + result := db.Preload("Identity").Preload("Cloud").Find(&defaults) if result.Error != nil { return nil, errors.E(op, dbError(result.Error)) } diff --git a/internal/db/model.go b/internal/db/model.go index cc4b737f9..69c4e58e5 100644 --- a/internal/db/model.go +++ b/internal/db/model.go @@ -43,7 +43,7 @@ func (d *Database) GetModel(ctx context.Context, model *dbmodel.Model) error { } else if model.ID != 0 { db = db.Where("id = ?", model.ID) } else if model.OwnerIdentityName != "" && model.Name != "" { - db = db.Where("owner_username = ? AND name = ?", model.OwnerIdentityName, model.Name) + db = db.Where("owner_identity_name = ? AND name = ?", model.OwnerIdentityName, model.Name) } else if model.ControllerID != 0 { // TODO(ales): fix ordering of where fields and handle error to represent what is *actually* required. db = db.Where("controller_id = ?", model.ControllerID) diff --git a/internal/db/user.go b/internal/db/user.go index 4a3276e3a..1b1899b6f 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -26,11 +26,11 @@ func (d *Database) GetUser(ctx context.Context, u *dbmodel.Identity) error { } if u.Name == "" { - return errors.E(op, errors.CodeNotFound, `invalid username ""`) + return errors.E(op, errors.CodeNotFound, `invalid identity name ""`) } db := d.DB.WithContext(ctx) - if err := db.Where("username = ?", u.Name).FirstOrCreate(&u).Error; err != nil { + if err := db.Where("name = ?", u.Name).FirstOrCreate(&u).Error; err != nil { return errors.E(op, err) } return nil @@ -47,11 +47,11 @@ func (d *Database) FetchUser(ctx context.Context, u *dbmodel.Identity) error { } if u.Name == "" { - return errors.E(op, errors.CodeNotFound, `invalid username ""`) + return errors.E(op, errors.CodeNotFound, `invalid identity name ""`) } db := d.DB.WithContext(ctx) - if err := db.Where("username = ?", u.Name).First(&u).Error; err != nil { + if err := db.Where("name = ?", u.Name).First(&u).Error; err != nil { return errors.E(op, err) } return nil diff --git a/internal/db/user_test.go b/internal/db/user_test.go index fbc910a2d..19de90250 100644 --- a/internal/db/user_test.go +++ b/internal/db/user_test.go @@ -32,7 +32,7 @@ func (s *dbSuite) TestGetUser(c *qt.C) { c.Assert(err, qt.IsNil) err = s.Database.GetUser(ctx, &dbmodel.Identity{}) - c.Check(err, qt.ErrorMatches, `invalid username ""`) + c.Check(err, qt.ErrorMatches, `invalid identity name ""`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) u := dbmodel.Identity{ @@ -68,7 +68,7 @@ func (s *dbSuite) TestUpdateUser(c *qt.C) { c.Assert(err, qt.IsNil) err = s.Database.UpdateUser(ctx, &dbmodel.Identity{}) - c.Check(err, qt.ErrorMatches, `invalid username ""`) + c.Check(err, qt.ErrorMatches, `invalid identity name ""`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) u := dbmodel.Identity{ diff --git a/internal/db/usermodeldefaults.go b/internal/db/usermodeldefaults.go index 9b2de0005..2629bcc2c 100644 --- a/internal/db/usermodeldefaults.go +++ b/internal/db/usermodeldefaults.go @@ -63,9 +63,9 @@ func (d *Database) UserModelDefaults(ctx context.Context, defaults *dbmodel.User } db := d.DB.WithContext(ctx) - db = db.Where("username = ?", defaults.IdentityName) + db = db.Where("identity_name = ?", defaults.IdentityName) - result := db.Preload("User").First(&defaults) + result := db.Preload("Identity").First(&defaults) if result.Error != nil { err := dbError(result.Error) if errors.ErrorCode(err) == errors.CodeNotFound { diff --git a/internal/dbmodel/user_test.go b/internal/dbmodel/user_test.go index 45d864892..61489b070 100644 --- a/internal/dbmodel/user_test.go +++ b/internal/dbmodel/user_test.go @@ -20,7 +20,7 @@ func TestUser(t *testing.T) { db := gormDB(c) var u0 dbmodel.Identity - result := db.Where("username = ?", "bob@external").First(&u0) + result := db.Where("name = ?", "bob@external").First(&u0) c.Check(result.Error, qt.Equals, gorm.ErrRecordNotFound) u1 := dbmodel.Identity{ @@ -32,7 +32,7 @@ func TestUser(t *testing.T) { c.Check(result.RowsAffected, qt.Equals, int64(1)) var u2 dbmodel.Identity - result = db.Where("username = ?", "bob@external").First(&u2) + result = db.Where("name = ?", "bob@external").First(&u2) c.Assert(result.Error, qt.IsNil) c.Check(u2, qt.DeepEquals, u1) @@ -41,7 +41,7 @@ func TestUser(t *testing.T) { result = db.Save(&u2) c.Assert(result.Error, qt.IsNil) var u3 dbmodel.Identity - result = db.Where("username = ?", "bob@external").First(&u3) + result = db.Where("name = ?", "bob@external").First(&u3) c.Assert(result.Error, qt.IsNil) c.Check(u3, qt.DeepEquals, u2) @@ -50,7 +50,7 @@ func TestUser(t *testing.T) { DisplayName: "bob", } result = db.Create(&u4) - c.Check(result.Error, qt.ErrorMatches, `.*violates unique constraint "users_username_key".*`) + c.Check(result.Error, qt.ErrorMatches, `.*violates unique constraint "identities_name_key".*`) } func TestUserTag(t *testing.T) { From e2c435cfc8449956450e528633bbe48798bd75ff Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 12 Jan 2024 12:55:57 +0000 Subject: [PATCH 019/126] Fix more Gorm magic strings Signed-off-by: Babak K. Shandiz --- internal/db/cloudcredential.go | 2 +- internal/dbmodel/model_test.go | 2 +- internal/jimmtest/cmp.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/db/cloudcredential.go b/internal/db/cloudcredential.go index 6b377879b..bd5aa0794 100644 --- a/internal/db/cloudcredential.go +++ b/internal/db/cloudcredential.go @@ -27,7 +27,7 @@ func (d *Database) SetCloudCredential(ctx context.Context, cred *dbmodel.CloudCr if err := db.Clauses(clause.OnConflict{ Columns: []clause.Column{ {Name: "cloud_name"}, - {Name: "owner_username"}, + {Name: "owner_identity_name"}, {Name: "name"}, }, DoUpdates: clause.AssignmentColumns([]string{"auth_type", "label", "attributes_in_vault", "attributes", "valid"}), diff --git a/internal/dbmodel/model_test.go b/internal/dbmodel/model_test.go index 60ba5080c..fcac5ceaf 100644 --- a/internal/dbmodel/model_test.go +++ b/internal/dbmodel/model_test.go @@ -57,7 +57,7 @@ func TestRecreateDeletedModel(t *testing.T) { CloudRegion: cl.Regions[0], CloudCredential: cred, } - c.Check(db.Create(&m2).Error, qt.ErrorMatches, `.*violates unique constraint "models_controller_id_owner_username_name_key".*`) + c.Check(db.Create(&m2).Error, qt.ErrorMatches, `.*violates unique constraint "models_controller_id_owner_identity_name_key".*`) c.Assert(db.Delete(&m1).Error, qt.IsNil) c.Check(db.First(&m1).Error, qt.Equals, gorm.ErrRecordNotFound) diff --git a/internal/jimmtest/cmp.go b/internal/jimmtest/cmp.go index 5413f7d63..404bef2fb 100644 --- a/internal/jimmtest/cmp.go +++ b/internal/jimmtest/cmp.go @@ -42,11 +42,11 @@ var DBObjectEquals = qt.CmpEquals( cmpopts.EquateEmpty(), cmpopts.IgnoreTypes(gorm.Model{}), cmpopts.IgnoreFields(dbmodel.Cloud{}, "ID", "CreatedAt", "UpdatedAt"), - cmpopts.IgnoreFields(dbmodel.CloudCredential{}, "CloudName", "OwnerUsername"), + cmpopts.IgnoreFields(dbmodel.CloudCredential{}, "CloudName", "OwnerIdentityName"), cmpopts.IgnoreFields(dbmodel.CloudRegion{}, "CloudName"), cmpopts.IgnoreFields(dbmodel.CloudRegionControllerPriority{}, "CloudRegionID", "ControllerID"), cmpopts.IgnoreFields(dbmodel.Controller{}, "ID"), - cmpopts.IgnoreFields(dbmodel.Model{}, "ID", "CreatedAt", "UpdatedAt", "OwnerUsername", "ControllerID", "CloudRegionID", "CloudCredentialID"), + cmpopts.IgnoreFields(dbmodel.Model{}, "ID", "CreatedAt", "UpdatedAt", "OwnerIdentityName", "ControllerID", "CloudRegionID", "CloudCredentialID"), ) // CmpEquals uses cmp.Diff (see http://godoc.org/github.com/google/go-cmp/cmp#Diff) From d4c282cb897dba74b92e766e0c8e30e687b30e88 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 12 Jan 2024 13:56:45 +0000 Subject: [PATCH 020/126] Fix column name reference Signed-off-by: Babak K. Shandiz --- internal/db/clouddefaults.go | 4 ++-- internal/db/usermodeldefaults.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/db/clouddefaults.go b/internal/db/clouddefaults.go index 8a2ade89a..49c6d3e30 100644 --- a/internal/db/clouddefaults.go +++ b/internal/db/clouddefaults.go @@ -46,7 +46,7 @@ func (d *Database) SetCloudDefaults(ctx context.Context, defaults *dbmodel.Cloud } if err := db.Clauses(clause.OnConflict{ Columns: []clause.Column{ - {Name: "username"}, + {Name: "identity_name"}, {Name: "cloud_id"}, {Name: "region"}, }, @@ -89,7 +89,7 @@ func (d *Database) UnsetCloudDefaults(ctx context.Context, defaults *dbmodel.Clo } if err := db.Clauses(clause.OnConflict{ Columns: []clause.Column{ - {Name: "username"}, + {Name: "identity_name"}, {Name: "cloud_id"}, {Name: "region"}, }, diff --git a/internal/db/usermodeldefaults.go b/internal/db/usermodeldefaults.go index 2629bcc2c..56ce2e5a3 100644 --- a/internal/db/usermodeldefaults.go +++ b/internal/db/usermodeldefaults.go @@ -40,7 +40,7 @@ func (d *Database) SetUserModelDefaults(ctx context.Context, defaults *dbmodel.U } if err := db.Clauses(clause.OnConflict{ Columns: []clause.Column{ - {Name: "username"}, + {Name: "identity_name"}, }, DoUpdates: clause.AssignmentColumns([]string{"defaults"}), }).Create(&dbDefaults).Error; err != nil { From c1ea33bf4a164066944570847b19afbdca0db863 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 12 Jan 2024 13:58:36 +0000 Subject: [PATCH 021/126] Update error message Signed-off-by: Babak K. Shandiz --- internal/db/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/db/user.go b/internal/db/user.go index 1b1899b6f..f105ad9e9 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -70,7 +70,7 @@ func (d *Database) UpdateUser(ctx context.Context, u *dbmodel.Identity) error { } if u.Name == "" { - return errors.E(op, errors.CodeNotFound, `invalid username ""`) + return errors.E(op, errors.CodeNotFound, `invalid identity name ""`) } db := d.DB.WithContext(ctx) From 64671ae89b3c9729388b7f5e98d70241d264cc09 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 15 Jan 2024 11:29:10 +0000 Subject: [PATCH 022/126] Rename indexes related to users Signed-off-by: Babak K. Shandiz --- internal/dbmodel/sql/postgres/1_6.sql | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql index a4cd033b8..d29af53ff 100644 --- a/internal/dbmodel/sql/postgres/1_6.sql +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -5,18 +5,28 @@ -- "When renaming a constraint that has an underlying index, the index is renamed as well." -- (See https://www.postgresql.org/docs/current/sql-altertable.html) -ALTER TABLE IF EXISTS users RENAME COLUMN username TO name; +-- Renaming tables/columns. ALTER TABLE IF EXISTS users RENAME TO identities; - +ALTER TABLE IF EXISTS identities RENAME COLUMN username TO name; ALTER TABLE IF EXISTS cloud_credentials RENAME COLUMN owner_username TO owner_identity_name; ALTER TABLE IF EXISTS cloud_defaults RENAME COLUMN username TO identity_name; ALTER TABLE IF EXISTS models RENAME COLUMN owner_username TO owner_identity_name; ALTER TABLE IF EXISTS application_offer_connections RENAME COLUMN username TO identity_name; ALTER TABLE IF EXISTS user_model_defaults RENAME COLUMN username TO identity_name; --- TODO (CSS-6701): Do we need to rename these two instances as well? +-- Renaming indexes: +ALTER INDEX IF EXISTS users_username_key RENAME TO identities_name_key; +ALTER INDEX IF EXISTS users_pkey RENAME TO identities_pkey; +ALTER INDEX IF EXISTS idx_users_deleted_at RENAME TO idx_identities_deleted_at; +ALTER INDEX IF EXISTS models_controller_id_owner_username_name_key RENAME TO models_controller_id_owner_identity_name_name_key; +ALTER INDEX IF EXISTS user_model_defaults_username_key RENAME TO user_model_defaults_identity_name_key; +ALTER INDEX IF EXISTS cloud_credentials_cloud_name_owner_username_name_key RENAME TO cloud_credentials_cloud_name_owner_identity_name_name_key; +ALTER INDEX IF EXISTS cloud_defaults_username_cloud_id_region_key RENAME TO cloud_defaults_identity_name_cloud_id_region_key; + +-- TODO (CSS-6701): Do we need to rename these instances as well? -- - ALTER TABLE IF EXISTS controllers RENAME COLUMN admin_user TO admin_identity_name; -- - ALTER TABLE IF EXISTS audit_log RENAME COLUMN user_tag TO identity_tag; +-- - ALTER INDEX IF EXISTS idx_audit_log_user_tag RENAME TO idx_audit_log_identity_tag -- We don't need to rename columns in these tables, because they're already -- dropped in an earlier migration: From 2c4d4a284ddb309a6c6b2bfd588cb1e82740d67e Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 15 Jan 2024 11:59:56 +0000 Subject: [PATCH 023/126] Fix constraint name reference Signed-off-by: Babak K. Shandiz --- internal/dbmodel/model_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/dbmodel/model_test.go b/internal/dbmodel/model_test.go index fcac5ceaf..59d740c65 100644 --- a/internal/dbmodel/model_test.go +++ b/internal/dbmodel/model_test.go @@ -57,7 +57,7 @@ func TestRecreateDeletedModel(t *testing.T) { CloudRegion: cl.Regions[0], CloudCredential: cred, } - c.Check(db.Create(&m2).Error, qt.ErrorMatches, `.*violates unique constraint "models_controller_id_owner_identity_name_key".*`) + c.Check(db.Create(&m2).Error, qt.ErrorMatches, `.*violates unique constraint "models_controller_id_owner_identity_name_name_key".*`) c.Assert(db.Delete(&m1).Error, qt.IsNil) c.Check(db.First(&m1).Error, qt.Equals, gorm.ErrRecordNotFound) From b497c0323e3c0617803db16e3a3ba1e1fde78ea6 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 15 Jan 2024 13:48:29 +0000 Subject: [PATCH 024/126] Rename `dbmodel/user*` files to `dbmodel/identity*` Signed-off-by: Babak K. Shandiz --- internal/dbmodel/{user.go => identity.go} | 0 internal/dbmodel/{user_test.go => identity_test.go} | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename internal/dbmodel/{user.go => identity.go} (100%) rename internal/dbmodel/{user_test.go => identity_test.go} (96%) diff --git a/internal/dbmodel/user.go b/internal/dbmodel/identity.go similarity index 100% rename from internal/dbmodel/user.go rename to internal/dbmodel/identity.go diff --git a/internal/dbmodel/user_test.go b/internal/dbmodel/identity_test.go similarity index 96% rename from internal/dbmodel/user_test.go rename to internal/dbmodel/identity_test.go index 61489b070..d9b0132df 100644 --- a/internal/dbmodel/user_test.go +++ b/internal/dbmodel/identity_test.go @@ -15,7 +15,7 @@ import ( "github.com/canonical/jimm/internal/dbmodel" ) -func TestUser(t *testing.T) { +func TestIdentity(t *testing.T) { c := qt.New(t) db := gormDB(c) @@ -66,7 +66,7 @@ func TestUserTag(t *testing.T) { c.Check(u2, qt.DeepEquals, u) } -func TestUserCloudCredentials(t *testing.T) { +func TestIdentityCloudCredentials(t *testing.T) { c := qt.New(t) db := gormDB(c) @@ -118,7 +118,7 @@ func TestUserCloudCredentials(t *testing.T) { }}) } -func TestUserToJujuUserInfo(t *testing.T) { +func TestIdentityToJujuUserInfo(t *testing.T) { c := qt.New(t) u := dbmodel.Identity{ From 23182a5390c6d8156819f236aeff4976f9f51945 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 15 Jan 2024 13:52:54 +0000 Subject: [PATCH 025/126] Rename `User` CRUD methods to `Identity` Signed-off-by: Babak K. Shandiz --- cmd/jimmctl/cmd/addcloudtocontroller_test.go | 2 +- cmd/jimmctl/cmd/jimmsuite_test.go | 6 +-- internal/db/db_test.go | 2 +- internal/db/user.go | 42 ++++++++++---------- internal/db/user_test.go | 42 ++++++++++---------- internal/jimm/access.go | 4 +- internal/jimm/cloud.go | 4 +- internal/jimm/cloudcredential.go | 2 +- internal/jimm/controller.go | 4 +- internal/jimm/model.go | 8 ++-- internal/jimm/user.go | 4 +- internal/jimm/user_test.go | 2 +- internal/jimmtest/env.go | 2 +- internal/jimmtest/suite.go | 6 +-- internal/jujuapi/controllerroot.go | 2 +- internal/jujuapi/jimm_test.go | 6 +-- 16 files changed, 69 insertions(+), 69 deletions(-) diff --git a/cmd/jimmctl/cmd/addcloudtocontroller_test.go b/cmd/jimmctl/cmd/addcloudtocontroller_test.go index 4603d5a5e..e99d92393 100644 --- a/cmd/jimmctl/cmd/addcloudtocontroller_test.go +++ b/cmd/jimmctl/cmd/addcloudtocontroller_test.go @@ -31,7 +31,7 @@ func (s *addCloudToControllerSuite) SetUpTest(c *gc.C) { s.jimmSuite.SetUpTest(c) // We add user bob, who is a JIMM administrator. - err := s.JIMM.Database.UpdateUser(context.Background(), &dbmodel.Identity{ + err := s.JIMM.Database.UpdateIdentity(context.Background(), &dbmodel.Identity{ DisplayName: "Bob", Name: "bob@external", }) diff --git a/cmd/jimmctl/cmd/jimmsuite_test.go b/cmd/jimmctl/cmd/jimmsuite_test.go index f75a3e026..620fd56d7 100644 --- a/cmd/jimmctl/cmd/jimmsuite_test.go +++ b/cmd/jimmctl/cmd/jimmsuite_test.go @@ -107,7 +107,7 @@ func (s *jimmSuite) SetUpTest(c *gc.C) { Name: "alice@external", LastLogin: db.Now(), } - err = s.JIMM.Database.GetUser(ctx, s.AdminUser) + err = s.JIMM.Database.GetIdentity(ctx, s.AdminUser) c.Assert(err, gc.Equals, nil) alice := openfga.NewUser(s.AdminUser, ofgaClient) @@ -211,7 +211,7 @@ func (s *jimmSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, Name: tag.Owner().Id(), } user := openfga.NewUser(&u, s.JIMM.OpenFGAClient) - err := s.JIMM.Database.GetUser(ctx, &u) + err := s.JIMM.Database.GetIdentity(ctx, &u) c.Assert(err, gc.Equals, nil) _, err = s.JIMM.UpdateCloudCredential(ctx, user, jimm.UpdateCloudCredentialArgs{ CredentialTag: tag, @@ -229,7 +229,7 @@ func (s *jimmSuite) AddModel(c *gc.C, owner names.UserTag, name string, cloud na }, s.OFGAClient, ) - err := s.JIMM.Database.GetUser(ctx, u.Identity) + err := s.JIMM.Database.GetIdentity(ctx, u.Identity) c.Assert(err, gc.Equals, nil) mi, err := s.JIMM.AddModel(ctx, u, &jimm.ModelCreateArgs{ Name: name, diff --git a/internal/db/db_test.go b/internal/db/db_test.go index ca0df08ee..0bb490e56 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -62,7 +62,7 @@ func (s *dbSuite) TestTransaction(c *qt.C) { err = s.Database.Transaction(func(d *db.Database) error { c.Check(d, qt.Not(qt.Equals), s.Database) - return d.GetUser(context.Background(), &dbmodel.Identity{Name: "bob@external"}) + return d.GetIdentity(context.Background(), &dbmodel.Identity{Name: "bob@external"}) }) c.Assert(err, qt.IsNil) diff --git a/internal/db/user.go b/internal/db/user.go index f105ad9e9..eaae49d85 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -9,18 +9,18 @@ import ( "github.com/canonical/jimm/internal/errors" ) -// GetUser loads the user details for the user identified by username. If -// necessary the user record will be created, in which case the user will +// GetIdentity loads the details for the identity identified by name. If +// necessary the identity record will be created, in which case the identity will // have access to no resources and the default add-model access on JIMM. // -// GetUser does not fill out the user's ApplicationOffers, Clouds, -// CloudCredentials, or Models associations. See GetUserApplicationOffers, -// GetUserClouds, GetUserCloudCredentials, and GetUserModels to retrieve +// GetIdentity does not fill out the identity's ApplicationOffers, Clouds, +// CloudCredentials, or Models associations. See GetIdentityApplicationOffers, +// GetIdentityClouds, GetIdentityCloudCredentials, and GetIdentityModels to retrieve // this information. // -// GetUser returns an error with CodeNotFound if the username is invalid. -func (d *Database) GetUser(ctx context.Context, u *dbmodel.Identity) error { - const op = errors.Op("db.GetUser") +// GetIdentity returns an error with CodeNotFound if the identity name is invalid. +func (d *Database) GetIdentity(ctx context.Context, u *dbmodel.Identity) error { + const op = errors.Op("db.GetIdentity") if err := d.ready(); err != nil { return errors.E(op, err) } @@ -36,12 +36,12 @@ func (d *Database) GetUser(ctx context.Context, u *dbmodel.Identity) error { return nil } -// FetchUser loads the user details for the user identified by username. It -// will not create a user if the user cannot be found. +// FetchIdentity loads the details for the identity identified by name. It +// will not create an identity if the identity cannot be found. // -// FetchUser returns an error with CodeNotFound if the username is invalid. -func (d *Database) FetchUser(ctx context.Context, u *dbmodel.Identity) error { - const op = errors.Op("db.FetchUser") +// FetchIdentity returns an error with CodeNotFound if the identity name is invalid. +func (d *Database) FetchIdentity(ctx context.Context, u *dbmodel.Identity) error { + const op = errors.Op("db.FetchIdentity") if err := d.ready(); err != nil { return errors.E(op, err) } @@ -57,14 +57,14 @@ func (d *Database) FetchUser(ctx context.Context, u *dbmodel.Identity) error { return nil } -// UpdateUser updates the given user record. UpdateUser will not store any -// changes to a user's ApplicationOffers, Clouds, CloudCredentials, or +// UpdateIdentity updates the given identity record. UpdateIdentity will not store any +// changes to an identity's ApplicationOffers, Clouds, CloudCredentials, or // Models. These should be updated through the object in question. // -// UpdateUser returns an error with CodeNotFound if the username is +// UpdateIdentity returns an error with CodeNotFound if the identity name is // invalid. -func (d *Database) UpdateUser(ctx context.Context, u *dbmodel.Identity) error { - const op = errors.Op("db.UpdateUser") +func (d *Database) UpdateIdentity(ctx context.Context, u *dbmodel.Identity) error { + const op = errors.Op("db.UpdateIdentity") if err := d.ready(); err != nil { return errors.E(op, err) } @@ -81,9 +81,9 @@ func (d *Database) UpdateUser(ctx context.Context, u *dbmodel.Identity) error { return nil } -// GetUserCloudCredentials fetches user cloud credentials for the specified cloud. -func (d *Database) GetUserCloudCredentials(ctx context.Context, u *dbmodel.Identity, cloud string) ([]dbmodel.CloudCredential, error) { - const op = errors.Op("db.GetUserCloudCredentials") +// GetIdentityCloudCredentials fetches identity's cloud credentials for the specified cloud. +func (d *Database) GetIdentityCloudCredentials(ctx context.Context, u *dbmodel.Identity, cloud string) ([]dbmodel.CloudCredential, error) { + const op = errors.Op("db.GetIdentityCloudCredentials") if err := d.ready(); err != nil { return nil, errors.E(op, err) } diff --git a/internal/db/user_test.go b/internal/db/user_test.go index 19de90250..d23c61437 100644 --- a/internal/db/user_test.go +++ b/internal/db/user_test.go @@ -13,101 +13,101 @@ import ( "github.com/canonical/jimm/internal/errors" ) -func TestGetUserUnconfiguredDatabase(t *testing.T) { +func TestGetIdentityUnconfiguredDatabase(t *testing.T) { c := qt.New(t) var d db.Database - err := d.GetUser(context.Background(), &dbmodel.Identity{}) + err := d.GetIdentity(context.Background(), &dbmodel.Identity{}) c.Check(err, qt.ErrorMatches, `database not configured`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeServerConfiguration) } -func (s *dbSuite) TestGetUser(c *qt.C) { +func (s *dbSuite) TestGetIdentity(c *qt.C) { ctx := context.Background() - err := s.Database.GetUser(ctx, &dbmodel.Identity{}) + err := s.Database.GetIdentity(ctx, &dbmodel.Identity{}) c.Check(err, qt.ErrorMatches, `upgrade in progress`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeUpgradeInProgress) err = s.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - err = s.Database.GetUser(ctx, &dbmodel.Identity{}) + err = s.Database.GetIdentity(ctx, &dbmodel.Identity{}) c.Check(err, qt.ErrorMatches, `invalid identity name ""`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) u := dbmodel.Identity{ Name: "bob@external", } - err = s.Database.GetUser(ctx, &u) + err = s.Database.GetIdentity(ctx, &u) c.Assert(err, qt.IsNil) u2 := dbmodel.Identity{ Name: u.Name, } - err = s.Database.GetUser(ctx, &u2) + err = s.Database.GetIdentity(ctx, &u2) c.Assert(err, qt.IsNil) c.Check(u2, qt.DeepEquals, u) } -func TestUpdateUserUnconfiguredDatabase(t *testing.T) { +func TestUpdateIdentityUnconfiguredDatabase(t *testing.T) { c := qt.New(t) var d db.Database - err := d.UpdateUser(context.Background(), &dbmodel.Identity{}) + err := d.UpdateIdentity(context.Background(), &dbmodel.Identity{}) c.Check(err, qt.ErrorMatches, `database not configured`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeServerConfiguration) } -func (s *dbSuite) TestUpdateUser(c *qt.C) { +func (s *dbSuite) TestUpdateIdentity(c *qt.C) { ctx := context.Background() - err := s.Database.UpdateUser(ctx, &dbmodel.Identity{}) + err := s.Database.UpdateIdentity(ctx, &dbmodel.Identity{}) c.Check(err, qt.ErrorMatches, `upgrade in progress`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeUpgradeInProgress) err = s.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - err = s.Database.UpdateUser(ctx, &dbmodel.Identity{}) + err = s.Database.UpdateIdentity(ctx, &dbmodel.Identity{}) c.Check(err, qt.ErrorMatches, `invalid identity name ""`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) u := dbmodel.Identity{ Name: "bob@external", } - err = s.Database.GetUser(ctx, &u) + err = s.Database.GetIdentity(ctx, &u) c.Assert(err, qt.IsNil) - err = s.Database.UpdateUser(ctx, &u) + err = s.Database.UpdateIdentity(ctx, &u) c.Assert(err, qt.IsNil) u2 := dbmodel.Identity{ Name: u.Name, } - err = s.Database.GetUser(ctx, &u2) + err = s.Database.GetIdentity(ctx, &u2) c.Assert(err, qt.IsNil) c.Check(u2, qt.DeepEquals, u) } -func TestGetUserCloudCredentialsUnconfiguredDatabase(t *testing.T) { +func TestGetIdentityCloudCredentialsUnconfiguredDatabase(t *testing.T) { c := qt.New(t) var d db.Database - _, err := d.GetUserCloudCredentials(context.Background(), &dbmodel.Identity{}, "") + _, err := d.GetIdentityCloudCredentials(context.Background(), &dbmodel.Identity{}, "") c.Check(err, qt.ErrorMatches, `database not configured`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeServerConfiguration) } -func (s *dbSuite) TestGetUserCloudCredentials(c *qt.C) { +func (s *dbSuite) TestGetIdentityCloudCredentials(c *qt.C) { ctx := context.Background() err := s.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - _, err = s.Database.GetUserCloudCredentials(ctx, &dbmodel.Identity{}, "") + _, err = s.Database.GetIdentityCloudCredentials(ctx, &dbmodel.Identity{}, "") c.Check(err, qt.ErrorMatches, `cloudcredential not found`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) - _, err = s.Database.GetUserCloudCredentials(ctx, &dbmodel.Identity{ + _, err = s.Database.GetIdentityCloudCredentials(ctx, &dbmodel.Identity{ Name: "test", }, "ec2") c.Check(err, qt.IsNil) @@ -144,7 +144,7 @@ func (s *dbSuite) TestGetUserCloudCredentials(c *qt.C) { err = s.Database.SetCloudCredential(context.Background(), &cred2) c.Assert(err, qt.Equals, nil) - credentials, err := s.Database.GetUserCloudCredentials(ctx, &u, cloud.Name) + credentials, err := s.Database.GetIdentityCloudCredentials(ctx, &u, cloud.Name) c.Check(err, qt.IsNil) c.Assert(credentials, qt.DeepEquals, []dbmodel.CloudCredential{cred1, cred2}) } diff --git a/internal/jimm/access.go b/internal/jimm/access.go index 148c04098..8803cb34e 100644 --- a/internal/jimm/access.go +++ b/internal/jimm/access.go @@ -319,7 +319,7 @@ func (j *JIMM) GrantAuditLogAccess(ctx context.Context, user *openfga.User, targ targetUser := &dbmodel.Identity{} targetUser.SetTag(targetUserTag) - err := j.Database.GetUser(ctx, targetUser) + err := j.Database.GetIdentity(ctx, targetUser) if err != nil { return errors.E(op, err) } @@ -342,7 +342,7 @@ func (j *JIMM) RevokeAuditLogAccess(ctx context.Context, user *openfga.User, tar targetUser := &dbmodel.Identity{} targetUser.SetTag(targetUserTag) - err := j.Database.GetUser(ctx, targetUser) + err := j.Database.GetIdentity(ctx, targetUser) if err != nil { return errors.E(op, err) } diff --git a/internal/jimm/cloud.go b/internal/jimm/cloud.go index 0a8b0d598..435c13fad 100644 --- a/internal/jimm/cloud.go +++ b/internal/jimm/cloud.go @@ -530,7 +530,7 @@ func (j *JIMM) GrantCloudAccess(ctx context.Context, user *openfga.User, ct name err = j.doCloudAdmin(ctx, user, ct, func(_ *dbmodel.Cloud, _ API) error { targetUser := &dbmodel.Identity{} targetUser.SetTag(ut) - if err := j.Database.GetUser(ctx, targetUser); err != nil { + if err := j.Database.GetIdentity(ctx, targetUser); err != nil { return err } targetOfgaUser := openfga.NewUser(targetUser, j.OpenFGAClient) @@ -595,7 +595,7 @@ func (j *JIMM) RevokeCloudAccess(ctx context.Context, user *openfga.User, ct nam err = j.doCloudAdmin(ctx, user, ct, func(_ *dbmodel.Cloud, _ API) error { targetUser := &dbmodel.Identity{} targetUser.SetTag(ut) - if err := j.Database.GetUser(ctx, targetUser); err != nil { + if err := j.Database.GetIdentity(ctx, targetUser); err != nil { return err } targetOfgaUser := openfga.NewUser(targetUser, j.OpenFGAClient) diff --git a/internal/jimm/cloudcredential.go b/internal/jimm/cloudcredential.go index 524b63f5e..9e28394ac 100644 --- a/internal/jimm/cloudcredential.go +++ b/internal/jimm/cloudcredential.go @@ -142,7 +142,7 @@ func (j *JIMM) UpdateCloudCredential(ctx context.Context, user *openfga.User, ar // ensure the user we are adding the credential for exists. var u2 dbmodel.Identity u2.SetTag(args.CredentialTag.Owner()) - if err := j.Database.GetUser(ctx, &u2); err != nil { + if err := j.Database.GetIdentity(ctx, &u2); err != nil { return result, errors.E(op, err) } } diff --git a/internal/jimm/controller.go b/internal/jimm/controller.go index 71fb24291..f6792581a 100644 --- a/internal/jimm/controller.go +++ b/internal/jimm/controller.go @@ -380,7 +380,7 @@ func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerNa } ownerUser := dbmodel.Identity{} ownerUser.SetTag(ownerTag) - err = j.Database.GetUser(ctx, &ownerUser) + err = j.Database.GetIdentity(ctx, &ownerUser) if err != nil { return errors.E(op, err) } @@ -412,7 +412,7 @@ func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerNa // credential against the cloud the model is deployed against. Even using the correct cloud for the // credential is not strictly necessary, but will help prevent the user thinking they can create new // models on the incoming cloud. - allCredentials, err := j.Database.GetUserCloudCredentials(ctx, &ownerUser, cloudTag.Id()) + allCredentials, err := j.Database.GetIdentityCloudCredentials(ctx, &ownerUser, cloudTag.Id()) if err != nil { return errors.E(op, err) } diff --git a/internal/jimm/model.go b/internal/jimm/model.go index 58873bf76..8ef868a46 100644 --- a/internal/jimm/model.go +++ b/internal/jimm/model.go @@ -403,7 +403,7 @@ func (b *modelBuilder) selectCloudCredentials() error { if b.cloud == nil { return errors.E("cloud not specified") } - credentials, err := b.jimm.Database.GetUserCloudCredentials(b.ctx, b.owner, b.cloud.Name) + credentials, err := b.jimm.Database.GetIdentityCloudCredentials(b.ctx, b.owner, b.cloud.Name) if err != nil { return errors.E(err, "failed to fetch user cloud credentials") } @@ -522,7 +522,7 @@ func (j *JIMM) AddModel(ctx context.Context, user *openfga.User, args *ModelCrea owner := &dbmodel.Identity{ Name: args.Owner.Id(), } - err = j.Database.GetUser(ctx, owner) + err = j.Database.GetIdentity(ctx, owner) if err != nil { return nil, errors.E(op, err) } @@ -852,7 +852,7 @@ func (j *JIMM) GrantModelAccess(ctx context.Context, user *openfga.User, mt name err = j.doModelAdmin(ctx, user, mt, func(_ *dbmodel.Model, _ API) error { targetUser := &dbmodel.Identity{} targetUser.SetTag(ut) - if err := j.Database.GetUser(ctx, targetUser); err != nil { + if err := j.Database.GetIdentity(ctx, targetUser); err != nil { return err } targetOfgaUser := openfga.NewUser(targetUser, j.OpenFGAClient) @@ -930,7 +930,7 @@ func (j *JIMM) RevokeModelAccess(ctx context.Context, user *openfga.User, mt nam err = j.doModel(ctx, user, mt, requiredAccess, func(_ *dbmodel.Model, _ API) error { targetUser := &dbmodel.Identity{} targetUser.SetTag(ut) - if err := j.Database.GetUser(ctx, targetUser); err != nil { + if err := j.Database.GetIdentity(ctx, targetUser); err != nil { return err } targetOfgaUser := openfga.NewUser(targetUser, j.OpenFGAClient) diff --git a/internal/jimm/user.go b/internal/jimm/user.go index 44c2dc3b4..43c1708a9 100644 --- a/internal/jimm/user.go +++ b/internal/jimm/user.go @@ -35,7 +35,7 @@ func (j *JIMM) Authenticate(ctx context.Context, req *jujuparams.LoginRequest) ( pu := dbmodel.Identity{ Name: u.Name, } - if err := tx.GetUser(ctx, &pu); err != nil { + if err := tx.GetIdentity(ctx, &pu); err != nil { return err } u.Model = pu.Model @@ -47,7 +47,7 @@ func (j *JIMM) Authenticate(ctx context.Context, req *jujuparams.LoginRequest) ( } pu.LastLogin.Time = j.Database.DB.Config.NowFunc() pu.LastLogin.Valid = true - return tx.UpdateUser(ctx, &pu) + return tx.UpdateIdentity(ctx, &pu) }) if err != nil { return nil, errors.E(op, err) diff --git a/internal/jimm/user_test.go b/internal/jimm/user_test.go index 98752d35e..f6ef6c934 100644 --- a/internal/jimm/user_test.go +++ b/internal/jimm/user_test.go @@ -78,7 +78,7 @@ func TestAuthenticate(t *testing.T) { u2 := dbmodel.Identity{ Name: "bob@external", } - err = j.Database.GetUser(ctx, &u2) + err = j.Database.GetIdentity(ctx, &u2) c.Assert(err, qt.IsNil) c.Check(u2, qt.DeepEquals, dbmodel.Identity{ diff --git a/internal/jimmtest/env.go b/internal/jimmtest/env.go index 732004362..de127429f 100644 --- a/internal/jimmtest/env.go +++ b/internal/jimmtest/env.go @@ -494,7 +494,7 @@ func (u *User) DBObject(c *qt.C, db db.Database) dbmodel.Identity { u.dbo.Name = u.Username u.dbo.DisplayName = u.DisplayName - err := db.UpdateUser(context.Background(), &u.dbo) + err := db.UpdateIdentity(context.Background(), &u.dbo) c.Assert(err, qt.IsNil) return u.dbo } diff --git a/internal/jimmtest/suite.go b/internal/jimmtest/suite.go index 6cf3287d1..da07b6513 100644 --- a/internal/jimmtest/suite.go +++ b/internal/jimmtest/suite.go @@ -92,7 +92,7 @@ func (s *JIMMSuite) SetUpTest(c *gc.C) { Name: "alice@external", LastLogin: db.Now(), } - err = s.JIMM.Database.GetUser(ctx, s.AdminUser) + err = s.JIMM.Database.GetIdentity(ctx, s.AdminUser) c.Assert(err, gc.Equals, nil) adminUser := openfga.NewUser(s.AdminUser, s.OFGAClient) @@ -179,7 +179,7 @@ func (s *JIMMSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, Name: tag.Owner().Id(), } user := openfga.NewUser(&u, s.JIMM.OpenFGAClient) - err := s.JIMM.Database.GetUser(ctx, &u) + err := s.JIMM.Database.GetIdentity(ctx, &u) c.Assert(err, gc.Equals, nil) _, err = s.JIMM.UpdateCloudCredential(ctx, user, jimm.UpdateCloudCredentialArgs{ CredentialTag: tag, @@ -194,7 +194,7 @@ func (s *JIMMSuite) AddModel(c *gc.C, owner names.UserTag, name string, cloud na u := dbmodel.Identity{ Name: owner.Id(), } - err := s.JIMM.Database.GetUser(ctx, &u) + err := s.JIMM.Database.GetIdentity(ctx, &u) c.Assert(err, gc.Equals, nil) mi, err := s.JIMM.AddModel(ctx, s.NewUser(&u), &jimm.ModelCreateArgs{ Name: name, diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index 8162b2b09..72dbf8583 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -149,7 +149,7 @@ func (r *controllerRoot) masquerade(ctx context.Context, userTag string) (*openf user := dbmodel.Identity{ Name: ut.Id(), } - if err := r.jimm.DB().GetUser(ctx, &user); err != nil { + if err := r.jimm.DB().GetIdentity(ctx, &user); err != nil { return nil, err } return openfga.NewUser(&user, r.jimm.AuthorizationClient()), nil diff --git a/internal/jujuapi/jimm_test.go b/internal/jujuapi/jimm_test.go index 5f1b724a9..345b40938 100644 --- a/internal/jujuapi/jimm_test.go +++ b/internal/jujuapi/jimm_test.go @@ -570,7 +570,7 @@ func (s *jimmSuite) TestAddCloudToController(c *gc.C) { u := dbmodel.Identity{ Name: "alice@external", } - err := s.JIMM.Database.GetUser(ctx, &u) + err := s.JIMM.Database.GetIdentity(ctx, &u) c.Assert(err, gc.IsNil) conn := s.open(c, nil, "alice@external") @@ -607,7 +607,7 @@ func (s *jimmSuite) TestAddExistingCloudToController(c *gc.C) { u := dbmodel.Identity{ Name: "alice@external", } - err := s.JIMM.Database.GetUser(ctx, &u) + err := s.JIMM.Database.GetIdentity(ctx, &u) c.Assert(err, gc.IsNil) conn := s.open(c, nil, "alice@external") @@ -655,7 +655,7 @@ func (s *jimmSuite) TestRemoveCloudFromController(c *gc.C) { u := dbmodel.Identity{ Name: "alice@external", } - err := s.JIMM.Database.GetUser(ctx, &u) + err := s.JIMM.Database.GetIdentity(ctx, &u) c.Assert(err, gc.IsNil) conn := s.open(c, nil, "alice@external") From 4327628143b86553ad7c90740cc670d9b4a1be7f Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 15 Jan 2024 13:54:32 +0000 Subject: [PATCH 026/126] Rename `db/user*` files to `db/identity*` Signed-off-by: Babak K. Shandiz --- internal/db/{user.go => identity.go} | 0 internal/db/{user_test.go => identity_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename internal/db/{user.go => identity.go} (100%) rename internal/db/{user_test.go => identity_test.go} (100%) diff --git a/internal/db/user.go b/internal/db/identity.go similarity index 100% rename from internal/db/user.go rename to internal/db/identity.go diff --git a/internal/db/user_test.go b/internal/db/identity_test.go similarity index 100% rename from internal/db/user_test.go rename to internal/db/identity_test.go From 6819014ec8e6b879a36f6b0bb3b7d432fbb98a42 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Tue, 16 Jan 2024 10:10:59 +0000 Subject: [PATCH 027/126] Use default database (`postgres`) for Candid Signed-off-by: Babak K. Shandiz --- local/candid/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local/candid/config.yaml b/local/candid/config.yaml index 53c53dcfc..5beede576 100644 --- a/local/candid/config.yaml +++ b/local/candid/config.yaml @@ -1,6 +1,6 @@ storage: type: postgres - connection-string: postgresql://jimm:jimm@db:5432/jimm?sslmode=disable + connection-string: postgresql://jimm:jimm@db:5432/postgres?sslmode=disable logging-config: "DEBUG" auth-username: admin auth-password: password From 64229abf3437feeb1a818e2edde397d7ccb3c513 Mon Sep 17 00:00:00 2001 From: Ales Stimec Date: Tue, 16 Jan 2024 00:54:50 +0200 Subject: [PATCH 028/126] Renamed UserModelDefaults to IdentityModelDefaults. --- ...eldefaults.go => identitymodeldefaults.go} | 18 ++--- ..._test.go => identitymodeldefaults_test.go} | 54 +++++++------- internal/db/pgx_test.go | 10 ++- ...eldefaults.go => identitymodeldefaults.go} | 4 +- internal/dbmodel/sql/postgres/1_6.sql | 1 + ...s_test.go => idenitymodeldefaults_test.go} | 72 +++++++++---------- internal/jimm/identitymodeldefaults.go | 45 ++++++++++++ internal/jimm/model.go | 2 +- internal/jimm/usermodeldefaults.go | 45 ------------ internal/jimmtest/env.go | 6 +- internal/jujuapi/controllerroot.go | 6 +- 11 files changed, 135 insertions(+), 128 deletions(-) rename internal/db/{usermodeldefaults.go => identitymodeldefaults.go} (68%) rename internal/db/{usermodeldefaults_test.go => identitymodeldefaults_test.go} (71%) rename internal/dbmodel/{usermodeldefaults.go => identitymodeldefaults.go} (67%) rename internal/jimm/{usermodeldefaults_test.go => idenitymodeldefaults_test.go} (71%) create mode 100644 internal/jimm/identitymodeldefaults.go delete mode 100644 internal/jimm/usermodeldefaults.go diff --git a/internal/db/usermodeldefaults.go b/internal/db/identitymodeldefaults.go similarity index 68% rename from internal/db/usermodeldefaults.go rename to internal/db/identitymodeldefaults.go index 56ce2e5a3..5f139b27f 100644 --- a/internal/db/usermodeldefaults.go +++ b/internal/db/identitymodeldefaults.go @@ -11,18 +11,18 @@ import ( "github.com/canonical/jimm/internal/errors" ) -// SetUserModelDefaults sets default model setting values for the controller. -func (d *Database) SetUserModelDefaults(ctx context.Context, defaults *dbmodel.UserModelDefaults) error { - const op = errors.Op("db.SetUserModelDefaults") +// SetIdentityModelDefaults sets default model setting values for the controller. +func (d *Database) SetIdentityModelDefaults(ctx context.Context, defaults *dbmodel.IdentityModelDefaults) error { + const op = errors.Op("db.SetIdentityModelDefaults") err := d.Transaction(func(d *Database) error { db := d.DB.WithContext(ctx) - dbDefaults := dbmodel.UserModelDefaults{ + dbDefaults := dbmodel.IdentityModelDefaults{ IdentityName: defaults.IdentityName, } // try to fetch cloud defaults from the db - err := d.UserModelDefaults(ctx, &dbDefaults) + err := d.IdentityModelDefaults(ctx, &dbDefaults) if err != nil { if errors.ErrorCode(err) == errors.CodeNotFound { // if defaults do not exist, we create them @@ -54,9 +54,9 @@ func (d *Database) SetUserModelDefaults(ctx context.Context, defaults *dbmodel.U return nil } -// UserModelDefaults fetches user defaults. -func (d *Database) UserModelDefaults(ctx context.Context, defaults *dbmodel.UserModelDefaults) error { - const op = errors.Op("db.UserModelDefaults") +// IdentityModelDefaults fetches identities defaults. +func (d *Database) IdentityModelDefaults(ctx context.Context, defaults *dbmodel.IdentityModelDefaults) error { + const op = errors.Op("db.IdentityModelDefaults") if err := d.ready(); err != nil { return errors.E(op, err) @@ -69,7 +69,7 @@ func (d *Database) UserModelDefaults(ctx context.Context, defaults *dbmodel.User if result.Error != nil { err := dbError(result.Error) if errors.ErrorCode(err) == errors.CodeNotFound { - return errors.E(op, errors.CodeNotFound, "usermodeldefaults not found", err) + return errors.E(op, errors.CodeNotFound, "identitymodeldefaults not found", err) } return errors.E(op, err) } diff --git a/internal/db/usermodeldefaults_test.go b/internal/db/identitymodeldefaults_test.go similarity index 71% rename from internal/db/usermodeldefaults_test.go rename to internal/db/identitymodeldefaults_test.go index d26eac4e9..df3e6af4c 100644 --- a/internal/db/usermodeldefaults_test.go +++ b/internal/db/identitymodeldefaults_test.go @@ -17,17 +17,17 @@ import ( "github.com/canonical/jimm/internal/jimmtest" ) -func TestSetUserModelDefaults(t *testing.T) { +func TestSetIdentityModelDefaults(t *testing.T) { c := qt.New(t) ctx := context.Background() now := time.Now() type testConfig struct { - user *dbmodel.Identity + identity *dbmodel.Identity defaults map[string]interface{} expectedError string - expectedDefaults *dbmodel.UserModelDefaults + expectedDefaults *dbmodel.IdentityModelDefaults } tests := []struct { @@ -37,24 +37,24 @@ func TestSetUserModelDefaults(t *testing.T) { }{{ about: "defaults do not exist yet - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ + identity := dbmodel.Identity{ Name: "bob@external", } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) defaults := map[string]interface{}{ "key1": float64(42), "key2": "a test string", } - expectedDefaults := dbmodel.UserModelDefaults{ - IdentityName: user.Name, - Identity: user, + expectedDefaults := dbmodel.IdentityModelDefaults{ + IdentityName: identity.Name, + Identity: identity, Defaults: defaults, } return testConfig{ - user: &user, + identity: &identity, defaults: defaults, expectedDefaults: &expectedDefaults, } @@ -62,14 +62,14 @@ func TestSetUserModelDefaults(t *testing.T) { }, { about: "defaults already exist - defaults updated", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ + identity := dbmodel.Identity{ Name: "bob@external", } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) - j.Database.SetUserModelDefaults(ctx, &dbmodel.UserModelDefaults{ - IdentityName: user.Name, - Identity: user, + j.Database.SetIdentityModelDefaults(ctx, &dbmodel.IdentityModelDefaults{ + IdentityName: identity.Name, + Identity: identity, Defaults: map[string]interface{}{ "key1": float64(17), "key2": "a test string", @@ -82,22 +82,22 @@ func TestSetUserModelDefaults(t *testing.T) { "key3": "a new value", } - expectedDefaults := dbmodel.UserModelDefaults{ - IdentityName: user.Name, - Identity: user, + expectedDefaults := dbmodel.IdentityModelDefaults{ + IdentityName: identity.Name, + Identity: identity, Defaults: defaults, } return testConfig{ - user: &user, + identity: &identity, defaults: defaults, expectedDefaults: &expectedDefaults, } }, }, { - about: "user does not exist", + about: "identity does not exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ + identity := dbmodel.Identity{ Name: "bob@external", } @@ -108,7 +108,7 @@ func TestSetUserModelDefaults(t *testing.T) { } return testConfig{ - user: &user, + identity: &identity, defaults: defaults, expectedError: `.*violates foreign key constraint.*`, } @@ -116,10 +116,10 @@ func TestSetUserModelDefaults(t *testing.T) { }, { about: "cannot set agent-version", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ + identity := dbmodel.Identity{ Name: "bob@external", } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) defaults := map[string]interface{}{ "agent-version": "2.0", @@ -128,7 +128,7 @@ func TestSetUserModelDefaults(t *testing.T) { } return testConfig{ - user: &user, + identity: &identity, defaults: defaults, expectedError: `agent-version cannot have a default value`, } @@ -147,13 +147,13 @@ func TestSetUserModelDefaults(t *testing.T) { testConfig := test.setup(c, j) - err = j.SetUserModelDefaults(ctx, testConfig.user, testConfig.defaults) + err = j.SetIdentityModelDefaults(ctx, testConfig.identity, testConfig.defaults) if testConfig.expectedError == "" { c.Assert(err, qt.Equals, nil) - dbDefaults := dbmodel.UserModelDefaults{ + dbDefaults := dbmodel.IdentityModelDefaults{ IdentityName: testConfig.expectedDefaults.IdentityName, } - err = j.Database.UserModelDefaults(ctx, &dbDefaults) + err = j.Database.IdentityModelDefaults(ctx, &dbDefaults) c.Assert(err, qt.Equals, nil) c.Assert(&dbDefaults, qt.CmpEquals(cmpopts.IgnoreTypes(gorm.Model{})), testConfig.expectedDefaults) } else { diff --git a/internal/db/pgx_test.go b/internal/db/pgx_test.go index 269138aaf..79df6eaf5 100644 --- a/internal/db/pgx_test.go +++ b/internal/db/pgx_test.go @@ -23,6 +23,10 @@ import ( "github.com/canonical/jimm/internal/jimmtest" ) +const ( + defaultDSN = "postgresql://jimm:jimm@127.0.0.1:5432/jimm" +) + func TestPostgres(t *testing.T) { c := qt.New(t) @@ -34,8 +38,10 @@ type postgresSuite struct { } func (s *postgresSuite) Init(c *qt.C) { - dsn, exists := os.LookupEnv("JIMM_TEST_PGXDSN") - c.Assert(exists, qt.IsTrue, qt.Commentf("env var JIMM_TEST_PGXDSN is not assigned")) + dsn := defaultDSN + if envTestDSN, exists := os.LookupEnv("JIMM_TEST_PGXDSN"); exists { + dsn = envTestDSN + } connCfg, err := pgx.ParseConfig(dsn) c.Assert(err, qt.IsNil) diff --git a/internal/dbmodel/usermodeldefaults.go b/internal/dbmodel/identitymodeldefaults.go similarity index 67% rename from internal/dbmodel/usermodeldefaults.go rename to internal/dbmodel/identitymodeldefaults.go index dc55ef2ac..04c966840 100644 --- a/internal/dbmodel/usermodeldefaults.go +++ b/internal/dbmodel/identitymodeldefaults.go @@ -4,8 +4,8 @@ package dbmodel import "gorm.io/gorm" -// UserModelDefaults holds user's model defaults. -type UserModelDefaults struct { +// IdentityModelDefaults holds identities's model defaults. +type IdentityModelDefaults struct { gorm.Model IdentityName string diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql index d29af53ff..b1e5510ce 100644 --- a/internal/dbmodel/sql/postgres/1_6.sql +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -22,6 +22,7 @@ ALTER INDEX IF EXISTS models_controller_id_owner_username_name_key RENAME TO mod ALTER INDEX IF EXISTS user_model_defaults_username_key RENAME TO user_model_defaults_identity_name_key; ALTER INDEX IF EXISTS cloud_credentials_cloud_name_owner_username_name_key RENAME TO cloud_credentials_cloud_name_owner_identity_name_name_key; ALTER INDEX IF EXISTS cloud_defaults_username_cloud_id_region_key RENAME TO cloud_defaults_identity_name_cloud_id_region_key; +ALTER TABLE IF EXISTS user_model_defaults RENAME TO identity_model_defaults; -- TODO (CSS-6701): Do we need to rename these instances as well? -- - ALTER TABLE IF EXISTS controllers RENAME COLUMN admin_user TO admin_identity_name; diff --git a/internal/jimm/usermodeldefaults_test.go b/internal/jimm/idenitymodeldefaults_test.go similarity index 71% rename from internal/jimm/usermodeldefaults_test.go rename to internal/jimm/idenitymodeldefaults_test.go index 737a4d471..99d94d284 100644 --- a/internal/jimm/usermodeldefaults_test.go +++ b/internal/jimm/idenitymodeldefaults_test.go @@ -17,17 +17,17 @@ import ( "github.com/canonical/jimm/internal/jimmtest" ) -func TestSetUserModelDefaults(t *testing.T) { +func TestSetIdentityModelDefaults(t *testing.T) { c := qt.New(t) ctx := context.Background() now := time.Now() type testConfig struct { - user *dbmodel.Identity + identity *dbmodel.Identity defaults map[string]interface{} expectedError string - expectedDefaults *dbmodel.UserModelDefaults + expectedDefaults *dbmodel.IdentityModelDefaults } tests := []struct { @@ -37,24 +37,24 @@ func TestSetUserModelDefaults(t *testing.T) { }{{ about: "defaults do not exist yet - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ + identity := dbmodel.Identity{ Name: "bob@external", } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) defaults := map[string]interface{}{ "key1": float64(42), "key2": "a test string", } - expectedDefaults := dbmodel.UserModelDefaults{ - IdentityName: user.Name, - Identity: user, + expectedDefaults := dbmodel.IdentityModelDefaults{ + IdentityName: identity.Name, + Identity: identity, Defaults: defaults, } return testConfig{ - user: &user, + identity: &identity, defaults: defaults, expectedDefaults: &expectedDefaults, } @@ -62,14 +62,14 @@ func TestSetUserModelDefaults(t *testing.T) { }, { about: "defaults already exist - defaults updated", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ + identity := dbmodel.Identity{ Name: "bob@external", } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) - j.Database.SetUserModelDefaults(ctx, &dbmodel.UserModelDefaults{ - IdentityName: user.Name, - Identity: user, + j.Database.SetIdentityModelDefaults(ctx, &dbmodel.IdentityModelDefaults{ + IdentityName: identity.Name, + Identity: identity, Defaults: map[string]interface{}{ "key1": float64(17), "key2": "a test string", @@ -82,14 +82,14 @@ func TestSetUserModelDefaults(t *testing.T) { "key3": "a new value", } - expectedDefaults := dbmodel.UserModelDefaults{ - IdentityName: user.Name, - Identity: user, + expectedDefaults := dbmodel.IdentityModelDefaults{ + IdentityName: identity.Name, + Identity: identity, Defaults: defaults, } return testConfig{ - user: &user, + identity: &identity, defaults: defaults, expectedDefaults: &expectedDefaults, } @@ -97,10 +97,10 @@ func TestSetUserModelDefaults(t *testing.T) { }, { about: "cannot set agent-version", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ + identity := dbmodel.Identity{ Name: "bob@external", } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) defaults := map[string]interface{}{ "agent-version": "2.0", @@ -109,7 +109,7 @@ func TestSetUserModelDefaults(t *testing.T) { } return testConfig{ - user: &user, + identity: &identity, defaults: defaults, expectedError: `agent-version cannot have a default value`, } @@ -128,13 +128,13 @@ func TestSetUserModelDefaults(t *testing.T) { testConfig := test.setup(c, j) - err = j.SetUserModelDefaults(ctx, testConfig.user, testConfig.defaults) + err = j.SetIdentityModelDefaults(ctx, testConfig.identity, testConfig.defaults) if testConfig.expectedError == "" { c.Assert(err, qt.Equals, nil) - dbDefaults := dbmodel.UserModelDefaults{ + dbDefaults := dbmodel.IdentityModelDefaults{ IdentityName: testConfig.expectedDefaults.IdentityName, } - err = j.Database.UserModelDefaults(ctx, &dbDefaults) + err = j.Database.IdentityModelDefaults(ctx, &dbDefaults) c.Assert(err, qt.Equals, nil) c.Assert(&dbDefaults, qt.CmpEquals(cmpopts.IgnoreTypes(gorm.Model{})), testConfig.expectedDefaults) } else { @@ -144,14 +144,14 @@ func TestSetUserModelDefaults(t *testing.T) { } } -func TestUserModelDefaults(t *testing.T) { +func TestIdentityModelDefaults(t *testing.T) { c := qt.New(t) ctx := context.Background() now := time.Now() type testConfig struct { - user *dbmodel.Identity + identity *dbmodel.Identity expectedError string expectedDefaults map[string]interface{} } @@ -163,27 +163,27 @@ func TestUserModelDefaults(t *testing.T) { }{{ about: "defaults do not exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ + identity := dbmodel.Identity{ Name: "bob@external", } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) return testConfig{ - user: &user, + identity: &identity, expectedError: "usermodeldefaults not found", } }, }, { about: "defaults exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ + identity := dbmodel.Identity{ Name: "bob@external", } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) - j.Database.SetUserModelDefaults(ctx, &dbmodel.UserModelDefaults{ - IdentityName: user.Name, - Identity: user, + j.Database.SetIdentityModelDefaults(ctx, &dbmodel.IdentityModelDefaults{ + IdentityName: identity.Name, + Identity: identity, Defaults: map[string]interface{}{ "key1": float64(42), "key2": "a changed string", @@ -192,7 +192,7 @@ func TestUserModelDefaults(t *testing.T) { }) return testConfig{ - user: &user, + identity: &identity, expectedDefaults: map[string]interface{}{ "key1": float64(42), "key2": "a changed string", @@ -214,7 +214,7 @@ func TestUserModelDefaults(t *testing.T) { testConfig := test.setup(c, j) - defaults, err := j.UserModelDefaults(ctx, testConfig.user) + defaults, err := j.IdentityModelDefaults(ctx, testConfig.identity) if testConfig.expectedError == "" { c.Assert(err, qt.Equals, nil) c.Assert(defaults, qt.CmpEquals(cmpopts.IgnoreTypes(gorm.Model{})), testConfig.expectedDefaults) diff --git a/internal/jimm/identitymodeldefaults.go b/internal/jimm/identitymodeldefaults.go new file mode 100644 index 000000000..af2c64e4a --- /dev/null +++ b/internal/jimm/identitymodeldefaults.go @@ -0,0 +1,45 @@ +// Copyright 2020 Canonical Ltd. + +package jimm + +import ( + "context" + + "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/errors" +) + +// SetIdentityModelDefaults writes new default model setting values for the user. +func (j *JIMM) SetIdentityModelDefaults(ctx context.Context, identity *dbmodel.Identity, configs map[string]interface{}) error { + const op = errors.Op("jimm.SetIdentityModelDefaults") + + for k := range configs { + if k == agentVersionKey { + return errors.E(op, errors.CodeBadRequest, `agent-version cannot have a default value`) + } + } + + err := j.Database.SetIdentityModelDefaults(ctx, &dbmodel.IdentityModelDefaults{ + IdentityName: identity.Name, + Defaults: configs, + }) + if err != nil { + return errors.E(op, err) + } + return nil +} + +// IdnetityModelDefaults returns the default config values for the identity. +func (j *JIMM) IdentityModelDefaults(ctx context.Context, identity *dbmodel.Identity) (map[string]interface{}, error) { + const op = errors.Op("jimm.UserModelDefaults") + + defaults := dbmodel.IdentityModelDefaults{ + IdentityName: identity.Name, + } + err := j.Database.IdentityModelDefaults(ctx, &defaults) + if err != nil { + return nil, errors.E(op, err) + } + + return defaults.Defaults, nil +} diff --git a/internal/jimm/model.go b/internal/jimm/model.go index 8ef868a46..cb17d0817 100644 --- a/internal/jimm/model.go +++ b/internal/jimm/model.go @@ -540,7 +540,7 @@ func (j *JIMM) AddModel(ctx context.Context, user *openfga.User, args *ModelCrea } // fetch user model defaults - userConfig, err := j.UserModelDefaults(ctx, user.Identity) + userConfig, err := j.IdentityModelDefaults(ctx, user.Identity) if err != nil && errors.ErrorCode(err) != errors.CodeNotFound { return nil, errors.E(op, "failed to fetch cloud defaults") } diff --git a/internal/jimm/usermodeldefaults.go b/internal/jimm/usermodeldefaults.go deleted file mode 100644 index 3540cb2a6..000000000 --- a/internal/jimm/usermodeldefaults.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2020 Canonical Ltd. - -package jimm - -import ( - "context" - - "github.com/canonical/jimm/internal/dbmodel" - "github.com/canonical/jimm/internal/errors" -) - -// SetUserModelDefaults writes new default model setting values for the user. -func (j *JIMM) SetUserModelDefaults(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error { - const op = errors.Op("jimm.SetUserModelDefaults") - - for k := range configs { - if k == agentVersionKey { - return errors.E(op, errors.CodeBadRequest, `agent-version cannot have a default value`) - } - } - - err := j.Database.SetUserModelDefaults(ctx, &dbmodel.UserModelDefaults{ - IdentityName: user.Name, - Defaults: configs, - }) - if err != nil { - return errors.E(op, err) - } - return nil -} - -// UserModelDefaults returns the default config values for the user. -func (j *JIMM) UserModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) { - const op = errors.Op("jimm.UserModelDefaults") - - defaults := dbmodel.UserModelDefaults{ - IdentityName: user.Name, - } - err := j.Database.UserModelDefaults(ctx, &defaults) - if err != nil { - return nil, errors.E(op, err) - } - - return defaults.Defaults, nil -} diff --git a/internal/jimmtest/env.go b/internal/jimmtest/env.go index de127429f..133536bd2 100644 --- a/internal/jimmtest/env.go +++ b/internal/jimmtest/env.go @@ -253,10 +253,10 @@ type UserDefaults struct { Defaults map[string]interface{} `json:"defaults"` env *Environment - dbo dbmodel.UserModelDefaults + dbo dbmodel.IdentityModelDefaults } -func (cd *UserDefaults) DBObject(c *qt.C, db db.Database) dbmodel.UserModelDefaults { +func (cd *UserDefaults) DBObject(c *qt.C, db db.Database) dbmodel.IdentityModelDefaults { if cd.dbo.ID != 0 { return cd.dbo } @@ -264,7 +264,7 @@ func (cd *UserDefaults) DBObject(c *qt.C, db db.Database) dbmodel.UserModelDefau cd.dbo.Identity = cd.env.User(cd.User).DBObject(c, db) cd.dbo.Defaults = cd.Defaults - err := db.SetUserModelDefaults(context.Background(), &cd.dbo) + err := db.SetIdentityModelDefaults(context.Background(), &cd.dbo) c.Assert(err, qt.IsNil) return cd.dbo } diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index 72dbf8583..98b4b7e76 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -60,9 +60,10 @@ type JIMM interface { GrantCloudAccess(ctx context.Context, user *openfga.User, ct names.CloudTag, ut names.UserTag, access string) error GrantModelAccess(ctx context.Context, user *openfga.User, mt names.ModelTag, ut names.UserTag, access jujuparams.UserAccessPermission) error GrantOfferAccess(ctx context.Context, u *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) error + IdentityModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) ImportModel(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error - InitiateMigration(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec, targetControllerID uint) (jujuparams.InitiateMigrationResult, error) InitiateInternalMigration(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetController string) (jujuparams.InitiateMigrationResult, error) + InitiateMigration(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec, targetControllerID uint) (jujuparams.InitiateMigrationResult, error) ListApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) ModelDefaultsForCloud(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) ModelInfo(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelInfo, error) @@ -82,14 +83,13 @@ type JIMM interface { RevokeOfferAccess(ctx context.Context, user *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) (err error) SetControllerConfig(ctx context.Context, u *openfga.User, args jujuparams.ControllerConfigSet) error SetControllerDeprecated(ctx context.Context, user *openfga.User, controllerName string, deprecated bool) error + SetIdentityModelDefaults(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error SetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, configs map[string]interface{}) error - SetUserModelDefaults(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error UnsetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error UpdateApplicationOffer(ctx context.Context, controller *dbmodel.Controller, offerUUID string, removed bool) error UpdateCloud(ctx context.Context, u *openfga.User, ct names.CloudTag, cloud jujuparams.Cloud) error UpdateCloudCredential(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) UpdateMigratedModel(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetControllerName string) error - UserModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) ValidateModelUpgrade(ctx context.Context, u *openfga.User, mt names.ModelTag, force bool) error WatchAllModelSummaries(ctx context.Context, controller *dbmodel.Controller) (_ func() error, err error) } From a69d6f5e3fc154970ce49e37a215961a1f7aea4b Mon Sep 17 00:00:00 2001 From: Ales Stimec Date: Tue, 16 Jan 2024 19:35:42 +0200 Subject: [PATCH 029/126] Fixes for identitymodeldefaults. --- internal/jimm/idenitymodeldefaults_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/jimm/idenitymodeldefaults_test.go b/internal/jimm/idenitymodeldefaults_test.go index 99d94d284..c6495e46f 100644 --- a/internal/jimm/idenitymodeldefaults_test.go +++ b/internal/jimm/idenitymodeldefaults_test.go @@ -170,7 +170,7 @@ func TestIdentityModelDefaults(t *testing.T) { return testConfig{ identity: &identity, - expectedError: "usermodeldefaults not found", + expectedError: "identitymodeldefaults not found", } }, }, { From f9b3ef0e47d04ee98349b6845c496c9834e56cb3 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Tue, 16 Jan 2024 20:37:53 +0200 Subject: [PATCH 030/126] Updated auth model (#1132) --- local/openfga/authorisation_model.fga | 18 ++++---- local/openfga/authorisation_model.json | 63 -------------------------- 2 files changed, 9 insertions(+), 72 deletions(-) diff --git a/local/openfga/authorisation_model.fga b/local/openfga/authorisation_model.fga index 365e17880..b1d90c74b 100644 --- a/local/openfga/authorisation_model.fga +++ b/local/openfga/authorisation_model.fga @@ -3,15 +3,15 @@ model type applicationoffer relations - define administrator: [serviceaccount, serviceaccount:*, user, user:*, group#member] or administrator from model - define consumer: [serviceaccount, serviceaccount:*, user, user:*, group#member] or administrator + define administrator: [user, user:*, group#member] or administrator from model + define consumer: [user, user:*, group#member] or administrator define model: [model] - define reader: [serviceaccount, serviceaccount:*, user, user:*, group#member] or consumer + define reader: [user, user:*, group#member] or consumer type cloud relations - define administrator: [serviceaccount, serviceaccount:*, user, user:*, group#member] or administrator from controller - define can_addmodel: [serviceaccount, serviceaccount:*, user, user:*, group#member] or administrator + define administrator: [user, user:*, group#member] or administrator from controller + define can_addmodel: [user, user:*, group#member] or administrator define controller: [controller] type controller @@ -26,13 +26,13 @@ type group type model relations - define administrator: [serviceaccount, serviceaccount:*, user, user:*, group#member] or administrator from controller + define administrator: [user, user:*, group#member] or administrator from controller define controller: [controller] - define reader: [serviceaccount, serviceaccount:*, user, user:*, group#member] or writer - define writer: [serviceaccount, serviceaccount:*, user, user:*, group#member] or administrator + define reader: [user, user:*, group#member] or writer + define writer: [user, user:*, group#member] or administrator type user type serviceaccount relations - define administator: [serviceaccount, serviceaccount:*, user, user:*, group#member] + define administator: [user, user:*, group#member] diff --git a/local/openfga/authorisation_model.json b/local/openfga/authorisation_model.json index 4ea5dff0b..ad39ba9f8 100644 --- a/local/openfga/authorisation_model.json +++ b/local/openfga/authorisation_model.json @@ -6,13 +6,6 @@ "relations": { "administrator": { "directly_related_user_types": [ - { - "type": "serviceaccount" - }, - { - "type": "serviceaccount", - "wildcard": {} - }, { "type": "user" }, @@ -28,13 +21,6 @@ }, "consumer": { "directly_related_user_types": [ - { - "type": "serviceaccount" - }, - { - "type": "serviceaccount", - "wildcard": {} - }, { "type": "user" }, @@ -57,13 +43,6 @@ }, "reader": { "directly_related_user_types": [ - { - "type": "serviceaccount" - }, - { - "type": "serviceaccount", - "wildcard": {} - }, { "type": "user" }, @@ -138,13 +117,6 @@ "relations": { "administrator": { "directly_related_user_types": [ - { - "type": "serviceaccount" - }, - { - "type": "serviceaccount", - "wildcard": {} - }, { "type": "user" }, @@ -160,13 +132,6 @@ }, "can_addmodel": { "directly_related_user_types": [ - { - "type": "serviceaccount" - }, - { - "type": "serviceaccount", - "wildcard": {} - }, { "type": "user" }, @@ -343,13 +308,6 @@ "relations": { "administrator": { "directly_related_user_types": [ - { - "type": "serviceaccount" - }, - { - "type": "serviceaccount", - "wildcard": {} - }, { "type": "user" }, @@ -372,13 +330,6 @@ }, "reader": { "directly_related_user_types": [ - { - "type": "serviceaccount" - }, - { - "type": "serviceaccount", - "wildcard": {} - }, { "type": "user" }, @@ -394,13 +345,6 @@ }, "writer": { "directly_related_user_types": [ - { - "type": "serviceaccount" - }, - { - "type": "serviceaccount", - "wildcard": {} - }, { "type": "user" }, @@ -478,13 +422,6 @@ "relations": { "administator": { "directly_related_user_types": [ - { - "type": "serviceaccount" - }, - { - "type": "serviceaccount", - "wildcard": {} - }, { "type": "user" }, From a996277210f6be050768a31c8e511fd8f93d1860 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Tue, 16 Jan 2024 16:47:19 -0800 Subject: [PATCH 031/126] CSS-6763 Introduce addServiceAccount method (#1127) * Introduce AddServiceAccount method * Fixed auth model and added tests * Update copyright date * PR changes * Correctly rename field * Check if user has permission over service account * Update service_account_test.go --- api/params/params.go | 8 ++++ internal/jimm/service_account.go | 53 +++++++++++++++++++++ internal/jimm/service_account_test.go | 48 +++++++++++++++++++ internal/jimmtest/jimm_mock.go | 9 ++++ internal/jujuapi/controllerroot.go | 1 + internal/jujuapi/jimm.go | 3 ++ internal/jujuapi/service_account.go | 24 ++++++++++ internal/jujuapi/service_account_test.go | 59 ++++++++++++++++++++++++ local/openfga/authorisation_model.json | 4 +- 9 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 internal/jimm/service_account.go create mode 100644 internal/jimm/service_account_test.go create mode 100644 internal/jujuapi/service_account.go create mode 100644 internal/jujuapi/service_account_test.go diff --git a/api/params/params.go b/api/params/params.go index 0d1f9a240..fc7bbced7 100644 --- a/api/params/params.go +++ b/api/params/params.go @@ -393,3 +393,11 @@ type MigrateModelInfo struct { type MigrateModelRequest struct { Specs []MigrateModelInfo `json:"specs"` } + +// Service Account related request parameters + +// AddServiceAccountRequest holds a request to add a service account. +type AddServiceAccountRequest struct { + // ClientID holds the client id of the service account. + ClientID string `json:"client-id"` +} diff --git a/internal/jimm/service_account.go b/internal/jimm/service_account.go new file mode 100644 index 000000000..52d803b88 --- /dev/null +++ b/internal/jimm/service_account.go @@ -0,0 +1,53 @@ +// Copyright 2024 Canonical Ltd. + +package jimm + +import ( + "context" + + "github.com/canonical/jimm/internal/errors" + "github.com/canonical/jimm/internal/openfga" + ofganames "github.com/canonical/jimm/internal/openfga/names" + jimmnames "github.com/canonical/jimm/pkg/names" +) + +// AddServiceAccount checks that no one owns the service account yet +// and then adds a relation between the logged in user and the service account. +func (j *JIMM) AddServiceAccount(ctx context.Context, u *openfga.User, clientId string) error { + op := errors.Op("jimm.AddServiceAccount") + svcTag := jimmnames.NewServiceAccountTag(clientId) + key := openfga.Tuple{ + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(svcTag), + } + keyWithUser := key + keyWithUser.Object = ofganames.ConvertTag(u.ResourceTag()) + + ok, err := j.OpenFGAClient.CheckRelation(ctx, keyWithUser, false) + if err != nil { + return errors.E(op, err) + } + // If the user already has administration permission over the + // service account then return early. + if ok { + return nil + } + + tuples, _, err := j.OpenFGAClient.ReadRelatedObjects(ctx, key, 10, "") + if err != nil { + return errors.E(op, err) + } + if len(tuples) > 0 { + return errors.E(op, "service account already owned") + } + addTuple := openfga.Tuple{ + Object: ofganames.ConvertTag(u.ResourceTag()), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(svcTag), + } + err = j.AuthorizationClient().AddRelation(ctx, addTuple) + if err != nil { + return errors.E(op, err) + } + return nil +} diff --git a/internal/jimm/service_account_test.go b/internal/jimm/service_account_test.go new file mode 100644 index 000000000..c86103e4e --- /dev/null +++ b/internal/jimm/service_account_test.go @@ -0,0 +1,48 @@ +// Copyright 2024 Canonical Ltd. + +package jimm_test + +import ( + "context" + "testing" + + qt "github.com/frankban/quicktest" + + "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/jimm" + "github.com/canonical/jimm/internal/jimmtest" + "github.com/canonical/jimm/internal/openfga" +) + +func TestAddServiceAccount(t *testing.T) { + c := qt.New(t) + + ctx := context.Background() + client, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) + c.Assert(err, qt.IsNil) + j := &jimm.JIMM{ + OpenFGAClient: client, + } + c.Assert(err, qt.IsNil) + user := openfga.NewUser( + &dbmodel.User{ + Username: "bob@external", + DisplayName: "Bob", + }, + client, + ) + clientID := "39caae91-b914-41ae-83f8-c7b86ca5ad5a" + err = j.AddServiceAccount(ctx, user, clientID) + c.Assert(err, qt.IsNil) + err = j.AddServiceAccount(ctx, user, clientID) + c.Assert(err, qt.IsNil) + userAlice := openfga.NewUser( + &dbmodel.User{ + Username: "alive@external", + DisplayName: "Alice", + }, + client, + ) + err = j.AddServiceAccount(ctx, userAlice, clientID) + c.Assert(err, qt.ErrorMatches, "service account already owned") +} diff --git a/internal/jimmtest/jimm_mock.go b/internal/jimmtest/jimm_mock.go index db9b53885..373ce895d 100644 --- a/internal/jimmtest/jimm_mock.go +++ b/internal/jimmtest/jimm_mock.go @@ -34,6 +34,7 @@ type JIMM struct { AddController_ func(ctx context.Context, u *openfga.User, ctl *dbmodel.Controller) error AddHostedCloud_ func(ctx context.Context, user *openfga.User, tag names.CloudTag, cloud jujuparams.Cloud, force bool) error AddModel_ func(ctx context.Context, u *openfga.User, args *jimm.ModelCreateArgs) (*jujuparams.ModelInfo, error) + AddServiceAccount_ func(ctx context.Context, u *openfga.User, clientId string) error Authenticate_ func(ctx context.Context, req *jujuparams.LoginRequest) (*openfga.User, error) AuthorizationClient_ func() *openfga.OFGAClient ChangeModelCredential_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, cloudCredentialTag names.CloudCredentialTag) error @@ -130,6 +131,14 @@ func (j *JIMM) AddModel(ctx context.Context, u *openfga.User, args *jimm.ModelCr } return j.AddModel_(ctx, u, args) } + +func (j *JIMM) AddServiceAccount(ctx context.Context, u *openfga.User, clientId string) error { + if j.AddServiceAccount_ == nil { + return errors.E(errors.CodeNotImplemented) + } + return j.AddServiceAccount_(ctx, u, clientId) +} + func (j *JIMM) Authenticate(ctx context.Context, req *jujuparams.LoginRequest) (*openfga.User, error) { if j.Authenticate_ == nil { return nil, errors.E(errors.CodeNotImplemented) diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index 9bbf4e679..b68115a07 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -29,6 +29,7 @@ type JIMM interface { AddController(ctx context.Context, u *openfga.User, ctl *dbmodel.Controller) error AddHostedCloud(ctx context.Context, user *openfga.User, tag names.CloudTag, cloud jujuparams.Cloud, force bool) error AddModel(ctx context.Context, u *openfga.User, args *jimm.ModelCreateArgs) (_ *jujuparams.ModelInfo, err error) + AddServiceAccount(ctx context.Context, u *openfga.User, clientId string) error Authenticate(ctx context.Context, req *jujuparams.LoginRequest) (*openfga.User, error) AuthorizationClient() *openfga.OFGAClient ChangeModelCredential(ctx context.Context, user *openfga.User, modelTag names.ModelTag, cloudCredentialTag names.CloudCredentialTag) error diff --git a/internal/jujuapi/jimm.go b/internal/jujuapi/jimm.go index dbbde615e..338bf47c9 100644 --- a/internal/jujuapi/jimm.go +++ b/internal/jujuapi/jimm.go @@ -49,6 +49,7 @@ func init() { crossModelQueryMethod := rpc.Method(r.CrossModelQuery) purgeLogsMethod := rpc.Method(r.PurgeLogs) migrateModel := rpc.Method(r.MigrateModel) + addServiceAccountMethod := rpc.Method(r.AddServiceAccount) // JIMM Generic RPC r.AddMethod("JIMM", 4, "AddController", addControllerMethod) @@ -77,6 +78,8 @@ func init() { r.AddMethod("JIMM", 4, "ListRelationshipTuples", listRelationshipTuplesMethod) // JIMM Cross-model queries r.AddMethod("JIMM", 4, "CrossModelQuery", crossModelQueryMethod) + // JIMM Service Accounts + r.AddMethod("JIMM", 4, "AddServiceAccount", addServiceAccountMethod) return []int{4} } diff --git a/internal/jujuapi/service_account.go b/internal/jujuapi/service_account.go new file mode 100644 index 000000000..2f3921c23 --- /dev/null +++ b/internal/jujuapi/service_account.go @@ -0,0 +1,24 @@ +// Copyright 2024 canonical. + +package jujuapi + +import ( + "context" + + apiparams "github.com/canonical/jimm/api/params" + "github.com/canonical/jimm/internal/errors" + jimmnames "github.com/canonical/jimm/pkg/names" +) + +// service_acount contains the primary RPC commands for handling service accounts within JIMM via the JIMM facade itself. + +// AddGroup creates a group within JIMMs DB for reference by OpenFGA. +func (r *controllerRoot) AddServiceAccount(ctx context.Context, req apiparams.AddServiceAccountRequest) error { + const op = errors.Op("jujuapi.AddGroup") + + if !jimmnames.IsValidServiceAccountId(req.ClientID) { + return errors.E(op, errors.CodeBadRequest, "invalid client ID") + } + + return r.jimm.AddServiceAccount(ctx, r.user, req.ClientID) +} diff --git a/internal/jujuapi/service_account_test.go b/internal/jujuapi/service_account_test.go new file mode 100644 index 000000000..8b472ae77 --- /dev/null +++ b/internal/jujuapi/service_account_test.go @@ -0,0 +1,59 @@ +// Copyright 2024 Canonical Ltd. + +package jujuapi_test + +import ( + "context" + "testing" + + "github.com/canonical/jimm/api/params" + "github.com/canonical/jimm/internal/jimmtest" + "github.com/canonical/jimm/internal/jujuapi" + "github.com/canonical/jimm/internal/openfga" + qt "github.com/frankban/quicktest" +) + +func TestAddServiceAccount(t *testing.T) { + c := qt.New(t) + + tests := []struct { + about string + addServiceAccount func(ctx context.Context, user *openfga.User, clientID string) error + args params.AddServiceAccountRequest + expectedError string + }{{ + about: "Valid client ID", + addServiceAccount: func(ctx context.Context, user *openfga.User, clientID string) error { + return nil + }, + args: params.AddServiceAccountRequest{ + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + }, + }, { + about: "Invalid Client ID", + addServiceAccount: func(ctx context.Context, user *openfga.User, clientID string) error { + return nil + }, + args: params.AddServiceAccountRequest{ + ClientID: "_123_", + }, + expectedError: "invalid client ID", + }} + + for _, test := range tests { + test := test + c.Run(test.about, func(c *qt.C) { + jimm := &jimmtest.JIMM{ + AddServiceAccount_: test.addServiceAccount, + } + cr := jujuapi.NewControllerRoot(jimm, jujuapi.Params{}) + + err := cr.AddServiceAccount(context.Background(), test.args) + if test.expectedError == "" { + c.Assert(err, qt.IsNil) + } else { + c.Assert(err, qt.ErrorMatches, test.expectedError) + } + }) + } +} diff --git a/local/openfga/authorisation_model.json b/local/openfga/authorisation_model.json index ad39ba9f8..7787b5ff0 100644 --- a/local/openfga/authorisation_model.json +++ b/local/openfga/authorisation_model.json @@ -420,7 +420,7 @@ { "metadata": { "relations": { - "administator": { + "administrator": { "directly_related_user_types": [ { "type": "user" @@ -438,7 +438,7 @@ } }, "relations": { - "administator": { + "administrator": { "this": {} } }, From 66daaccc42937673d7332146a62855b149c9295f Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 17 Jan 2024 11:59:40 +0000 Subject: [PATCH 032/126] Rename misspelled file name Signed-off-by: Babak K. Shandiz --- ...idenitymodeldefaults_test.go => identitymodeldefaults_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/jimm/{idenitymodeldefaults_test.go => identitymodeldefaults_test.go} (100%) diff --git a/internal/jimm/idenitymodeldefaults_test.go b/internal/jimm/identitymodeldefaults_test.go similarity index 100% rename from internal/jimm/idenitymodeldefaults_test.go rename to internal/jimm/identitymodeldefaults_test.go From 5bc41e482be06193062839add79d21d85645e440 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 17 Jan 2024 12:05:04 +0000 Subject: [PATCH 033/126] Change the ordering of table and column renaming to match others Signed-off-by: Babak K. Shandiz --- internal/dbmodel/sql/postgres/1_6.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql index b1e5510ce..1e4188fe6 100644 --- a/internal/dbmodel/sql/postgres/1_6.sql +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -12,7 +12,8 @@ ALTER TABLE IF EXISTS cloud_credentials RENAME COLUMN owner_username TO owner_id ALTER TABLE IF EXISTS cloud_defaults RENAME COLUMN username TO identity_name; ALTER TABLE IF EXISTS models RENAME COLUMN owner_username TO owner_identity_name; ALTER TABLE IF EXISTS application_offer_connections RENAME COLUMN username TO identity_name; -ALTER TABLE IF EXISTS user_model_defaults RENAME COLUMN username TO identity_name; +ALTER TABLE IF EXISTS user_model_defaults RENAME TO identity_model_defaults; +ALTER TABLE IF EXISTS identity_model_defaults RENAME COLUMN username TO identity_name; -- Renaming indexes: ALTER INDEX IF EXISTS users_username_key RENAME TO identities_name_key; @@ -22,7 +23,6 @@ ALTER INDEX IF EXISTS models_controller_id_owner_username_name_key RENAME TO mod ALTER INDEX IF EXISTS user_model_defaults_username_key RENAME TO user_model_defaults_identity_name_key; ALTER INDEX IF EXISTS cloud_credentials_cloud_name_owner_username_name_key RENAME TO cloud_credentials_cloud_name_owner_identity_name_name_key; ALTER INDEX IF EXISTS cloud_defaults_username_cloud_id_region_key RENAME TO cloud_defaults_identity_name_cloud_id_region_key; -ALTER TABLE IF EXISTS user_model_defaults RENAME TO identity_model_defaults; -- TODO (CSS-6701): Do we need to rename these instances as well? -- - ALTER TABLE IF EXISTS controllers RENAME COLUMN admin_user TO admin_identity_name; From 226090e208b99e7e6fba74ae2b454cf378e98ac4 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 17 Jan 2024 12:12:35 +0000 Subject: [PATCH 034/126] Rename `user_model_defaults_username_fkey` to `identity_*` Signed-off-by: Babak K. Shandiz --- internal/dbmodel/sql/postgres/1_6.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql index 1e4188fe6..985d22f0a 100644 --- a/internal/dbmodel/sql/postgres/1_6.sql +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -21,6 +21,7 @@ ALTER INDEX IF EXISTS users_pkey RENAME TO identities_pkey; ALTER INDEX IF EXISTS idx_users_deleted_at RENAME TO idx_identities_deleted_at; ALTER INDEX IF EXISTS models_controller_id_owner_username_name_key RENAME TO models_controller_id_owner_identity_name_name_key; ALTER INDEX IF EXISTS user_model_defaults_username_key RENAME TO user_model_defaults_identity_name_key; +ALTER INDEX IF EXISTS user_model_defaults_username_fkey RENAME TO user_model_defaults_identity_name_fkey; ALTER INDEX IF EXISTS cloud_credentials_cloud_name_owner_username_name_key RENAME TO cloud_credentials_cloud_name_owner_identity_name_name_key; ALTER INDEX IF EXISTS cloud_defaults_username_cloud_id_region_key RENAME TO cloud_defaults_identity_name_cloud_id_region_key; From 96d2988813231aa46f640559f09094048a053f98 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 17 Jan 2024 14:28:11 +0000 Subject: [PATCH 035/126] Rename `audit_log.user_tag` column to `identity_tag` Signed-off-by: Babak K. Shandiz --- internal/dbmodel/sql/postgres/1_6.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql index 985d22f0a..c1b7bc32a 100644 --- a/internal/dbmodel/sql/postgres/1_6.sql +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -14,6 +14,7 @@ ALTER TABLE IF EXISTS models RENAME COLUMN owner_username TO owner_identity_name ALTER TABLE IF EXISTS application_offer_connections RENAME COLUMN username TO identity_name; ALTER TABLE IF EXISTS user_model_defaults RENAME TO identity_model_defaults; ALTER TABLE IF EXISTS identity_model_defaults RENAME COLUMN username TO identity_name; +ALTER TABLE IF EXISTS audit_log RENAME COLUMN user_tag TO identity_tag; -- Renaming indexes: ALTER INDEX IF EXISTS users_username_key RENAME TO identities_name_key; @@ -24,11 +25,10 @@ ALTER INDEX IF EXISTS user_model_defaults_username_key RENAME TO user_model_defa ALTER INDEX IF EXISTS user_model_defaults_username_fkey RENAME TO user_model_defaults_identity_name_fkey; ALTER INDEX IF EXISTS cloud_credentials_cloud_name_owner_username_name_key RENAME TO cloud_credentials_cloud_name_owner_identity_name_name_key; ALTER INDEX IF EXISTS cloud_defaults_username_cloud_id_region_key RENAME TO cloud_defaults_identity_name_cloud_id_region_key; +ALTER INDEX IF EXISTS idx_audit_log_user_tag RENAME TO idx_audit_log_identity_tag; -- TODO (CSS-6701): Do we need to rename these instances as well? -- - ALTER TABLE IF EXISTS controllers RENAME COLUMN admin_user TO admin_identity_name; --- - ALTER TABLE IF EXISTS audit_log RENAME COLUMN user_tag TO identity_tag; --- - ALTER INDEX IF EXISTS idx_audit_log_user_tag RENAME TO idx_audit_log_identity_tag -- We don't need to rename columns in these tables, because they're already -- dropped in an earlier migration: From 77a2eae10844439adea29ba7115ca6e0ccef4d4a Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 17 Jan 2024 14:28:45 +0000 Subject: [PATCH 036/126] Rename `AuditLog.UserTag` to `IdentityTag` Signed-off-by: Babak K. Shandiz --- cmd/jimmctl/cmd/purge_logs_test.go | 12 +++++------ internal/db/audit.go | 10 ++++----- internal/db/auditlog_test.go | 34 +++++++++++++++--------------- internal/dbmodel/audit.go | 6 +++--- internal/dbmodel/audit_test.go | 4 ++-- internal/jimm/audit_log.go | 2 +- internal/jimm/jimm_test.go | 14 ++++++------ internal/jujuapi/jimm.go | 2 +- internal/jujuapi/jimm_test.go | 16 +++++++------- internal/rpc/client_test.go | 4 ++-- internal/rpc/proxy.go | 2 +- 11 files changed, 53 insertions(+), 53 deletions(-) diff --git a/cmd/jimmctl/cmd/purge_logs_test.go b/cmd/jimmctl/cmd/purge_logs_test.go index d41425ca4..49268816a 100644 --- a/cmd/jimmctl/cmd/purge_logs_test.go +++ b/cmd/jimmctl/cmd/purge_logs_test.go @@ -59,16 +59,16 @@ func (s *purgeLogsSuite) TestPurgeLogsFromDb(c *gc.C) { ctx := context.Background() relativeNow := time.Now().AddDate(-1, 0, 0) ale := dbmodel.AuditLogEntry{ - Time: relativeNow.UTC().Round(time.Millisecond), - UserTag: names.NewUserTag("alice@external").String(), + Time: relativeNow.UTC().Round(time.Millisecond), + IdentityTag: names.NewUserTag("alice@external").String(), } ale_past := dbmodel.AuditLogEntry{ - Time: relativeNow.AddDate(0, 0, -1).UTC().Round(time.Millisecond), - UserTag: names.NewUserTag("alice@external").String(), + Time: relativeNow.AddDate(0, 0, -1).UTC().Round(time.Millisecond), + IdentityTag: names.NewUserTag("alice@external").String(), } ale_future := dbmodel.AuditLogEntry{ - Time: relativeNow.AddDate(0, 0, 5).UTC().Round(time.Millisecond), - UserTag: names.NewUserTag("alice@external").String(), + Time: relativeNow.AddDate(0, 0, 5).UTC().Round(time.Millisecond), + IdentityTag: names.NewUserTag("alice@external").String(), } err := s.JIMM.Database.Migrate(context.Background(), false) diff --git a/internal/db/audit.go b/internal/db/audit.go index 27a0b3413..92a037a16 100644 --- a/internal/db/audit.go +++ b/internal/db/audit.go @@ -36,9 +36,9 @@ type AuditLogFilter struct { // found. End time.Time - // UserTag defines the user-tag on the audit log entry to match, if - // this is empty all user-tags are matched. - UserTag string + // IdentityTag defines the identity-tag on the audit log entry to match, if + // this is empty all identity-tags are matched. + IdentityTag string // Model is used to filter the event log to only contain events that // were performed against a specific model. @@ -77,8 +77,8 @@ func (d *Database) ForEachAuditLogEntry(ctx context.Context, filter AuditLogFilt if !filter.End.IsZero() { db = db.Where("time <= ?", filter.End) } - if filter.UserTag != "" { - db = db.Where("user_tag = ?", filter.UserTag) + if filter.IdentityTag != "" { + db = db.Where("identity_tag = ?", filter.IdentityTag) } if filter.Model != "" { db = db.Where("model = ?", filter.Model) diff --git a/internal/db/auditlog_test.go b/internal/db/auditlog_test.go index bbcfa60e4..55d2606c4 100644 --- a/internal/db/auditlog_test.go +++ b/internal/db/auditlog_test.go @@ -28,8 +28,8 @@ func (s *dbSuite) TestAddAuditLogEntry(c *qt.C) { ctx := context.Background() ale := dbmodel.AuditLogEntry{ - Time: time.Now().UTC().Round(time.Millisecond), - UserTag: names.NewUserTag("alice@external").String(), + Time: time.Now().UTC().Round(time.Millisecond), + IdentityTag: names.NewUserTag("alice@external").String(), } err := s.Database.AddAuditLogEntry(ctx, &ale) @@ -63,17 +63,17 @@ func TestForEachAuditLogEntryUnconfiguredDatabase(t *testing.T) { } var testAuditLogEntries = []dbmodel.AuditLogEntry{{ - Time: time.Date(2020, time.February, 20, 20, 2, 20, 0, time.UTC), - UserTag: names.NewUserTag("alice@external").String(), + Time: time.Date(2020, time.February, 20, 20, 2, 20, 0, time.UTC), + IdentityTag: names.NewUserTag("alice@external").String(), }, { - Time: time.Date(2020, time.February, 20, 20, 2, 21, 0, time.UTC), - UserTag: names.NewUserTag("alice@external").String(), + Time: time.Date(2020, time.February, 20, 20, 2, 21, 0, time.UTC), + IdentityTag: names.NewUserTag("alice@external").String(), }, { - Time: time.Date(2020, time.February, 20, 20, 2, 21, 0, time.UTC), - UserTag: names.NewUserTag("bob@external").String(), + Time: time.Date(2020, time.February, 20, 20, 2, 21, 0, time.UTC), + IdentityTag: names.NewUserTag("bob@external").String(), }, { - Time: time.Date(2020, time.February, 20, 20, 2, 23, 0, time.UTC), - UserTag: names.NewUserTag("alice@external").String(), + Time: time.Date(2020, time.February, 20, 20, 2, 23, 0, time.UTC), + IdentityTag: names.NewUserTag("alice@external").String(), }} var forEachAuditLogEntryTests = []struct { @@ -106,7 +106,7 @@ var forEachAuditLogEntryTests = []struct { }, { name: "UserTagFilter", filter: db.AuditLogFilter{ - UserTag: names.NewUserTag("alice@external").String(), + IdentityTag: names.NewUserTag("alice@external").String(), }, expectEntries: []int{0, 1, 3}, }} @@ -209,16 +209,16 @@ func (s *dbSuite) TestPurgeLogsFromDb(c *qt.C) { ctx := context.Background() relativeNow := time.Now().AddDate(-1, 0, 0) ale := dbmodel.AuditLogEntry{ - Time: relativeNow.UTC().Round(time.Millisecond), - UserTag: names.NewUserTag("alice@external").String(), + Time: relativeNow.UTC().Round(time.Millisecond), + IdentityTag: names.NewUserTag("alice@external").String(), } ale_past := dbmodel.AuditLogEntry{ - Time: relativeNow.AddDate(0, 0, -1).UTC().Round(time.Millisecond), - UserTag: names.NewUserTag("alice@external").String(), + Time: relativeNow.AddDate(0, 0, -1).UTC().Round(time.Millisecond), + IdentityTag: names.NewUserTag("alice@external").String(), } ale_future := dbmodel.AuditLogEntry{ - Time: relativeNow.AddDate(0, 0, 5).UTC().Round(time.Millisecond), - UserTag: names.NewUserTag("alice@external").String(), + Time: relativeNow.AddDate(0, 0, 5).UTC().Round(time.Millisecond), + IdentityTag: names.NewUserTag("alice@external").String(), } err := s.Database.Migrate(context.Background(), false) diff --git a/internal/dbmodel/audit.go b/internal/dbmodel/audit.go index ff705fa16..00b744510 100644 --- a/internal/dbmodel/audit.go +++ b/internal/dbmodel/audit.go @@ -40,8 +40,8 @@ type AuditLogEntry struct { // ObjectId contains the object id to act on, only used by certain facades. ObjectId string - // UserTag is the tag of the user the performed the action. - UserTag string `gorm:"index"` + // IdentityTag is the tag of the identity that performed the action. + IdentityTag string `gorm:"index"` // IsResponse indicates whether the action was a Response/Request. IsResponse bool @@ -69,7 +69,7 @@ func (e AuditLogEntry) ToAPIAuditEvent() apiparams.AuditEvent { ale.FacadeName = e.FacadeName ale.FacadeVersion = e.FacadeVersion ale.ObjectId = e.ObjectId - ale.UserTag = e.UserTag + ale.UserTag = e.IdentityTag ale.Model = e.Model ale.IsResponse = e.IsResponse ale.Errors = nil diff --git a/internal/dbmodel/audit_test.go b/internal/dbmodel/audit_test.go index 08221907b..6c52bacec 100644 --- a/internal/dbmodel/audit_test.go +++ b/internal/dbmodel/audit_test.go @@ -30,7 +30,7 @@ func TestAuditLogEntry(t *testing.T) { FacadeMethod: "AddController", FacadeVersion: 1, ObjectId: "1", - UserTag: names.NewUserTag("bob@external").String(), + IdentityTag: names.NewUserTag("bob@external").String(), IsResponse: false, Params: paramsJSON, Errors: nil, @@ -57,7 +57,7 @@ func TestToAPIAuditEvent(t *testing.T) { FacadeMethod: "AddController", FacadeVersion: 1, ObjectId: "1", - UserTag: names.NewUserTag("bob@external").String(), + IdentityTag: names.NewUserTag("bob@external").String(), IsResponse: false, Params: paramsJSON, Errors: nil, diff --git a/internal/jimm/audit_log.go b/internal/jimm/audit_log.go index 314768b04..286a3fb9d 100644 --- a/internal/jimm/audit_log.go +++ b/internal/jimm/audit_log.go @@ -45,7 +45,7 @@ func (r DbAuditLogger) newAuditLogEntry(header *rpc.Header) dbmodel.AuditLogEntr ale := dbmodel.AuditLogEntry{ Time: time.Now().UTC().Round(time.Millisecond), MessageId: header.RequestId, - UserTag: r.getUser().String(), + IdentityTag: r.getUser().String(), ConversationId: r.conversationId, } return ale diff --git a/internal/jimm/jimm_test.go b/internal/jimm/jimm_test.go index 8ef5e5364..4a91813c7 100644 --- a/internal/jimm/jimm_test.go +++ b/internal/jimm/jimm_test.go @@ -57,20 +57,20 @@ func TestFindAuditEvents(t *testing.T) { events := []dbmodel.AuditLogEntry{{ Time: now, - UserTag: admin.Identity.Tag().String(), + IdentityTag: admin.Identity.Tag().String(), FacadeMethod: "Login", }, { Time: now.Add(time.Hour), - UserTag: admin.Identity.Tag().String(), + IdentityTag: admin.Identity.Tag().String(), FacadeMethod: "AddModel", }, { Time: now.Add(2 * time.Hour), - UserTag: privileged.Identity.Tag().String(), + IdentityTag: privileged.Identity.Tag().String(), Model: "TestModel", FacadeMethod: "Deploy", }, { Time: now.Add(3 * time.Hour), - UserTag: privileged.Identity.Tag().String(), + IdentityTag: privileged.Identity.Tag().String(), Model: "TestModel", FacadeMethod: "DestroyModel", }} @@ -98,7 +98,7 @@ func TestFindAuditEvents(t *testing.T) { about: "admin/privileged user is allowed to find audit events by user", users: []*openfga.User{admin, privileged}, filter: db.AuditLogFilter{ - UserTag: admin.Tag().String(), + IdentityTag: admin.Tag().String(), }, expectedEvents: []dbmodel.AuditLogEntry{events[0], events[1]}, }, { @@ -135,13 +135,13 @@ func TestFindAuditEvents(t *testing.T) { about: "admin/privileged user - no events found", users: []*openfga.User{admin, privileged}, filter: db.AuditLogFilter{ - UserTag: "no-such-user", + IdentityTag: "no-such-user", }, }, { about: "unprivileged user is not allowed to access audit events", users: []*openfga.User{unprivileged}, filter: db.AuditLogFilter{ - UserTag: admin.Tag().String(), + IdentityTag: admin.Tag().String(), }, expectedError: "unauthorized", }} diff --git a/internal/jujuapi/jimm.go b/internal/jujuapi/jimm.go index dbbde615e..7fcb0fc8d 100644 --- a/internal/jujuapi/jimm.go +++ b/internal/jujuapi/jimm.go @@ -290,7 +290,7 @@ func auditParamsToFilter(req apiparams.FindAuditEventsRequest) (db.AuditLogFilte if err != nil { return filter, errors.E(err, errors.CodeBadRequest, `invalid "user-tag" filter`) } - filter.UserTag = tag.String() + filter.IdentityTag = tag.String() } limit := int(req.Limit) diff --git a/internal/jujuapi/jimm_test.go b/internal/jujuapi/jimm_test.go index 345b40938..08b1b62e0 100644 --- a/internal/jujuapi/jimm_test.go +++ b/internal/jujuapi/jimm_test.go @@ -415,14 +415,14 @@ func TestAuditLogAPIParamsConversion(t *testing.T) { SortTime: false, }, result: db.AuditLogFilter{ - Start: time.Date(2023, 8, 14, 0, 0, 0, 0, time.UTC), - End: time.Date(2023, 8, 14, 0, 0, 0, 0, time.UTC), - UserTag: "user-alice", - Model: "123", - Method: "Deploy", - Offset: 10, - Limit: 10, - SortTime: false, + Start: time.Date(2023, 8, 14, 0, 0, 0, 0, time.UTC), + End: time.Date(2023, 8, 14, 0, 0, 0, 0, time.UTC), + IdentityTag: "user-alice", + Model: "123", + Method: "Deploy", + Offset: 10, + Limit: 10, + SortTime: false, }, }, { about: "Test limit lower bound", diff --git a/internal/rpc/client_test.go b/internal/rpc/client_test.go index ae203ee93..96383013e 100644 --- a/internal/rpc/client_test.go +++ b/internal/rpc/client_test.go @@ -380,7 +380,7 @@ func TestProxySocketsAuditLogs(t *testing.T) { FacadeMethod: "TestReq", FacadeVersion: 0, ObjectId: "", - UserTag: "user-testUser", + IdentityTag: "user-testUser", IsResponse: false, Params: dbmodel.JSON(p), Errors: nil, @@ -394,7 +394,7 @@ func TestProxySocketsAuditLogs(t *testing.T) { FacadeMethod: "", FacadeVersion: 0, ObjectId: "", - UserTag: "user-testUser", + IdentityTag: "user-testUser", IsResponse: true, Params: nil, Errors: auditLogs[1].Errors, diff --git a/internal/rpc/proxy.go b/internal/rpc/proxy.go index c7e056ebd..2e02a841d 100644 --- a/internal/rpc/proxy.go +++ b/internal/rpc/proxy.go @@ -124,7 +124,7 @@ func (p *modelProxy) auditLogMessage(msg *message, isResponse bool) error { ale := dbmodel.AuditLogEntry{ Time: time.Now().UTC().Round(time.Millisecond), MessageId: msg.RequestID, - UserTag: p.tokenGen.GetUser().String(), + IdentityTag: p.tokenGen.GetUser().String(), Model: p.modelName, ConversationId: p.conversationId, FacadeName: msg.Type, From 418a569084fa505971f982b6abbccaeefa3be512 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 17 Jan 2024 14:31:15 +0000 Subject: [PATCH 037/126] Rename `controllers.admin_user` column to `admin_identity_name` Signed-off-by: Babak K. Shandiz --- internal/dbmodel/sql/postgres/1_6.sql | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql index c1b7bc32a..b93ff7530 100644 --- a/internal/dbmodel/sql/postgres/1_6.sql +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -14,6 +14,7 @@ ALTER TABLE IF EXISTS models RENAME COLUMN owner_username TO owner_identity_name ALTER TABLE IF EXISTS application_offer_connections RENAME COLUMN username TO identity_name; ALTER TABLE IF EXISTS user_model_defaults RENAME TO identity_model_defaults; ALTER TABLE IF EXISTS identity_model_defaults RENAME COLUMN username TO identity_name; +ALTER TABLE IF EXISTS controllers RENAME COLUMN admin_user TO admin_identity_name; ALTER TABLE IF EXISTS audit_log RENAME COLUMN user_tag TO identity_tag; -- Renaming indexes: @@ -27,9 +28,6 @@ ALTER INDEX IF EXISTS cloud_credentials_cloud_name_owner_username_name_key RENAM ALTER INDEX IF EXISTS cloud_defaults_username_cloud_id_region_key RENAME TO cloud_defaults_identity_name_cloud_id_region_key; ALTER INDEX IF EXISTS idx_audit_log_user_tag RENAME TO idx_audit_log_identity_tag; --- TODO (CSS-6701): Do we need to rename these instances as well? --- - ALTER TABLE IF EXISTS controllers RENAME COLUMN admin_user TO admin_identity_name; - -- We don't need to rename columns in these tables, because they're already -- dropped in an earlier migration: -- - user_application_offer_access From 9611d80a90a0a3115bb6b0c344de28ba30582489 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 17 Jan 2024 14:33:15 +0000 Subject: [PATCH 038/126] Rename `Controller.AdminUser` to `AdminIdentityName` Signed-off-by: Babak K. Shandiz --- cmd/jimmctl/cmd/jimmsuite_test.go | 12 ++++---- internal/dbmodel/controller.go | 8 +++--- internal/dbmodel/controller_test.go | 16 +++++------ internal/jimm/controller.go | 4 +-- internal/jimm/controller_test.go | 36 ++++++++++++------------ internal/jimm/jimm.go | 2 +- internal/jimmtest/env.go | 6 ++-- internal/jimmtest/suite.go | 12 ++++---- internal/jujuapi/jimm.go | 12 ++++---- internal/jujuclient/client_test.go | 12 ++++---- internal/jujuclient/dial_test.go | 12 ++++---- internal/jujuclient/modelwatcher_test.go | 12 ++++---- internal/jujuclient/ping_test.go | 12 ++++---- internal/jujuclient/storage_test.go | 36 ++++++++++++------------ 14 files changed, 96 insertions(+), 96 deletions(-) diff --git a/cmd/jimmctl/cmd/jimmsuite_test.go b/cmd/jimmctl/cmd/jimmsuite_test.go index 620fd56d7..9fc1c5b0a 100644 --- a/cmd/jimmctl/cmd/jimmsuite_test.go +++ b/cmd/jimmctl/cmd/jimmsuite_test.go @@ -183,12 +183,12 @@ func (s *jimmSuite) userBakeryClient(username string) *httpbakery.Client { func (s *jimmSuite) AddController(c *gc.C, name string, info *api.Info) { ctl := &dbmodel.Controller{ - UUID: info.ControllerUUID, - Name: name, - AdminUser: info.Tag.Id(), - AdminPassword: info.Password, - CACertificate: info.CACert, - Addresses: nil, + UUID: info.ControllerUUID, + Name: name, + AdminIdentityName: info.Tag.Id(), + AdminPassword: info.Password, + CACertificate: info.CACert, + Addresses: nil, } ctl.Addresses = make(dbmodel.HostPorts, 0, len(info.Addrs)) for _, addr := range info.Addrs { diff --git a/internal/dbmodel/controller.go b/internal/dbmodel/controller.go index 941b4ec41..3ceacca81 100644 --- a/internal/dbmodel/controller.go +++ b/internal/dbmodel/controller.go @@ -28,9 +28,9 @@ type Controller struct { // purposes. UUID string `gorm:"not null"` - // AdminUser is the username that JIMM uses to connect to the + // AdminIdentityName is the identity name that JIMM uses to connect to the // controller. - AdminUser string + AdminIdentityName string // AdminPassword is the password that JIMM uses to connect to the // controller. @@ -104,7 +104,7 @@ func (c Controller) ToAPIControllerInfo() apiparams.ControllerInfo { var ci apiparams.ControllerInfo ci.Name = c.Name ci.UUID = c.UUID - ci.Username = c.AdminUser + ci.Username = c.AdminIdentityName ci.PublicAddress = c.PublicAddress for _, hps := range c.Addresses { for _, hp := range hps { @@ -114,7 +114,7 @@ func (c Controller) ToAPIControllerInfo() apiparams.ControllerInfo { ci.CACertificate = c.CACertificate ci.CloudTag = names.NewCloudTag(c.CloudName).String() ci.CloudRegion = c.CloudRegion - ci.Username = c.AdminUser + ci.Username = c.AdminIdentityName ci.AgentVersion = c.AgentVersion if c.UnavailableSince.Valid { ci.Status = jujuparams.EntityStatus{ diff --git a/internal/dbmodel/controller_test.go b/internal/dbmodel/controller_test.go index 068668a82..720abb730 100644 --- a/internal/dbmodel/controller_test.go +++ b/internal/dbmodel/controller_test.go @@ -39,13 +39,13 @@ func TestController(t *testing.T) { c.Assert(result.Error, qt.IsNil) ctl := dbmodel.Controller{ - Name: "test-controller", - UUID: "00000000-0000-0000-0000-000000000001", - AdminUser: "admin", - AdminPassword: "pw", - CACertificate: "ca-cert", - PublicAddress: "controller.example.com:443", - CloudName: "test-cloud", + Name: "test-controller", + UUID: "00000000-0000-0000-0000-000000000001", + AdminIdentityName: "admin", + AdminPassword: "pw", + CACertificate: "ca-cert", + PublicAddress: "controller.example.com:443", + CloudName: "test-cloud", Addresses: dbmodel.HostPorts([][]jujuparams.HostPort{{{ Address: jujuparams.Address{ Value: "1.1.1.1", @@ -166,7 +166,7 @@ func TestToAPIControllerInfo(t *testing.T) { CloudRegion: cl.Regions[0], Priority: dbmodel.CloudRegionControllerPriorityDeployed, }} - ctl.AdminUser = "admin" + ctl.AdminIdentityName = "admin" ctl.AgentVersion = "1.2.3" ci := ctl.ToAPIControllerInfo() diff --git a/internal/jimm/controller.go b/internal/jimm/controller.go index f6792581a..1b637fa33 100644 --- a/internal/jimm/controller.go +++ b/internal/jimm/controller.go @@ -92,7 +92,7 @@ func (j *JIMM) AddController(ctx context.Context, user *openfga.User, ctl *dbmod credentialsStored := false if j.CredentialStore != nil { - err := j.CredentialStore.PutControllerCredentials(ctx, ctl.Name, ctl.AdminUser, ctl.AdminPassword) + err := j.CredentialStore.PutControllerCredentials(ctx, ctl.Name, ctl.AdminIdentityName, ctl.AdminPassword) if err != nil { return errors.E(op, err, "failed to store controller credentials") } @@ -145,7 +145,7 @@ func (j *JIMM) AddController(ctx context.Context, user *openfga.User, ctl *dbmod // if we already stored controller credentials in CredentialStore // we should not store them plain text in JIMM's DB. if credentialsStored { - ctl.AdminUser = "" + ctl.AdminIdentityName = "" ctl.AdminPassword = "" } if err := tx.AddController(ctx, ctl); err != nil { diff --git a/internal/jimm/controller_test.go b/internal/jimm/controller_test.go index 0680f80fd..3d94c0e7a 100644 --- a/internal/jimm/controller_test.go +++ b/internal/jimm/controller_test.go @@ -157,10 +157,10 @@ func TestAddController(t *testing.T) { c.Assert(err, qt.IsNil) ctl1 := dbmodel.Controller{ - Name: "test-controller", - AdminUser: "admin", - AdminPassword: "5ecret", - PublicAddress: "example.com:443", + Name: "test-controller", + AdminIdentityName: "admin", + AdminPassword: "5ecret", + PublicAddress: "example.com:443", } err = j.AddController(context.Background(), alice, &ctl1) c.Assert(err, qt.IsNil) @@ -173,10 +173,10 @@ func TestAddController(t *testing.T) { c.Check(ctl2, qt.CmpEquals(cmpopts.EquateEmpty(), cmpopts.IgnoreTypes(dbmodel.CloudRegion{})), ctl1) ctl3 := dbmodel.Controller{ - Name: "test-controller-2", - AdminUser: "admin", - AdminPassword: "5ecret", - PublicAddress: "example.com:443", + Name: "test-controller-2", + AdminIdentityName: "admin", + AdminPassword: "5ecret", + PublicAddress: "example.com:443", } err = j.AddController(context.Background(), alice, &ctl3) c.Assert(err, qt.IsNil) @@ -325,14 +325,14 @@ func TestAddControllerWithVault(t *testing.T) { c.Assert(err, qt.IsNil) ctl1 := dbmodel.Controller{ - Name: "test-controller", - AdminUser: "admin", - AdminPassword: "5ecret", - PublicAddress: "example.com:443", + Name: "test-controller", + AdminIdentityName: "admin", + AdminPassword: "5ecret", + PublicAddress: "example.com:443", } err = j.AddController(context.Background(), alice, &ctl1) c.Assert(err, qt.IsNil) - c.Assert(ctl1.AdminUser, qt.Equals, "") + c.Assert(ctl1.AdminIdentityName, qt.Equals, "") c.Assert(ctl1.AdminPassword, qt.Equals, "") ctl2 := dbmodel.Controller{ @@ -348,14 +348,14 @@ func TestAddControllerWithVault(t *testing.T) { c.Assert(password, qt.Equals, "5ecret") ctl3 := dbmodel.Controller{ - Name: "test-controller-2", - AdminUser: "admin", - AdminPassword: "5ecretToo", - PublicAddress: "example.com:443", + Name: "test-controller-2", + AdminIdentityName: "admin", + AdminPassword: "5ecretToo", + PublicAddress: "example.com:443", } err = j.AddController(context.Background(), alice, &ctl3) c.Assert(err, qt.IsNil) - c.Assert(ctl3.AdminUser, qt.Equals, "") + c.Assert(ctl3.AdminIdentityName, qt.Equals, "") c.Assert(ctl3.AdminPassword, qt.Equals, "") ctl4 := dbmodel.Controller{ diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index 60fb94de3..742a389b0 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -517,7 +517,7 @@ func fillMigrationTarget(db db.Database, credStore credentials.CredentialStore, if err != nil { return jujuparams.MigrationTargetInfo{}, 0, err } - adminUser := dbController.AdminUser + adminUser := dbController.AdminIdentityName adminPass := dbController.AdminPassword if adminPass == "" { u, p, err := credStore.GetControllerCredentials(ctx, controllerName) diff --git a/internal/jimmtest/env.go b/internal/jimmtest/env.go index 133536bd2..4d5e2133c 100644 --- a/internal/jimmtest/env.go +++ b/internal/jimmtest/env.go @@ -180,9 +180,9 @@ func (m Model) addModelRelations(c *qt.C, jimmTag names.ControllerTag, db db.Dat // addControllerRelations adds permissions the model should have and adds permissions for users to the controller. func (ctl Controller) addControllerRelations(c *qt.C, jimmTag names.ControllerTag, db db.Database, client *openfga.OFGAClient) { - if ctl.dbo.AdminUser != "" { + if ctl.dbo.AdminIdentityName != "" { user := openfga.NewUser(&dbmodel.Identity{ - Name: ctl.dbo.AdminUser, + Name: ctl.dbo.AdminIdentityName, }, client) err := user.SetControllerAccess(context.Background(), ctl.dbo.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) @@ -387,7 +387,7 @@ func (ctl *Controller) DBObject(c *qt.C, db db.Database) dbmodel.Controller { ctl.dbo.Name = ctl.Name ctl.dbo.UUID = ctl.UUID ctl.dbo.AgentVersion = ctl.AgentVersion - ctl.dbo.AdminUser = ctl.AdminUser + ctl.dbo.AdminIdentityName = ctl.AdminUser ctl.dbo.AdminPassword = ctl.AdminPassword ctl.dbo.CloudName = ctl.Cloud ctl.dbo.CloudRegion = ctl.CloudRegion diff --git a/internal/jimmtest/suite.go b/internal/jimmtest/suite.go index da07b6513..4f3602e1b 100644 --- a/internal/jimmtest/suite.go +++ b/internal/jimmtest/suite.go @@ -151,12 +151,12 @@ func (s *JIMMSuite) NewUser(u *dbmodel.Identity) *openfga.User { func (s *JIMMSuite) AddController(c *gc.C, name string, info *api.Info) { ctl := &dbmodel.Controller{ - UUID: info.ControllerUUID, - Name: name, - AdminUser: info.Tag.Id(), - AdminPassword: info.Password, - CACertificate: info.CACert, - Addresses: nil, + UUID: info.ControllerUUID, + Name: name, + AdminIdentityName: info.Tag.Id(), + AdminPassword: info.Password, + CACertificate: info.CACert, + Addresses: nil, } ctl.Addresses = make(dbmodel.HostPorts, 0, len(info.Addrs)) for _, addr := range info.Addrs { diff --git a/internal/jujuapi/jimm.go b/internal/jujuapi/jimm.go index 7fcb0fc8d..70033cada 100644 --- a/internal/jujuapi/jimm.go +++ b/internal/jujuapi/jimm.go @@ -165,12 +165,12 @@ func (r *controllerRoot) AddController(ctx context.Context, req apiparams.AddCon } ctl := dbmodel.Controller{ - UUID: req.UUID, - Name: req.Name, - PublicAddress: req.PublicAddress, - CACertificate: req.CACertificate, - AdminUser: req.Username, - AdminPassword: req.Password, + UUID: req.UUID, + Name: req.Name, + PublicAddress: req.PublicAddress, + CACertificate: req.CACertificate, + AdminIdentityName: req.Username, + AdminPassword: req.Password, } nphps, err := network.ParseProviderHostPorts(req.APIAddresses...) if err != nil { diff --git a/internal/jujuclient/client_test.go b/internal/jujuclient/client_test.go index 5abec889d..243bc0880 100644 --- a/internal/jujuclient/client_test.go +++ b/internal/jujuclient/client_test.go @@ -38,12 +38,12 @@ func (s *clientSuite) TestStatus(c *gc.C) { info := s.APIInfo(c) ctl := dbmodel.Controller{ - UUID: info.ControllerUUID, - Name: s.ControllerConfig.ControllerName(), - CACertificate: info.CACert, - AdminUser: info.Tag.Id(), - AdminPassword: info.Password, - PublicAddress: info.Addrs[0], + UUID: info.ControllerUUID, + Name: s.ControllerConfig.ControllerName(), + CACertificate: info.CACert, + AdminIdentityName: info.Tag.Id(), + AdminPassword: info.Password, + PublicAddress: info.Addrs[0], } models, err := s.API.UpdateCredential(ctx, cred) diff --git a/internal/jujuclient/dial_test.go b/internal/jujuclient/dial_test.go index cdf7db353..7ba5bc3f8 100644 --- a/internal/jujuclient/dial_test.go +++ b/internal/jujuclient/dial_test.go @@ -71,12 +71,12 @@ var _ = gc.Suite(&dialSuite{}) func (s *dialSuite) TestDial(c *gc.C) { info := s.APIInfo(c) ctl := dbmodel.Controller{ - UUID: s.ControllerConfig.ControllerUUID(), - Name: s.ControllerConfig.ControllerName(), - CACertificate: info.CACert, - AdminUser: info.Tag.Id(), - AdminPassword: info.Password, - PublicAddress: info.Addrs[0], + UUID: s.ControllerConfig.ControllerUUID(), + Name: s.ControllerConfig.ControllerName(), + CACertificate: info.CACert, + AdminIdentityName: info.Tag.Id(), + AdminPassword: info.Password, + PublicAddress: info.Addrs[0], } api, err := s.Dialer.Dial(context.Background(), &ctl, names.ModelTag{}, nil) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuclient/modelwatcher_test.go b/internal/jujuclient/modelwatcher_test.go index b2797846d..81f11d518 100644 --- a/internal/jujuclient/modelwatcher_test.go +++ b/internal/jujuclient/modelwatcher_test.go @@ -42,12 +42,12 @@ func (s *modelWatcherSuite) SetUpTest(c *gc.C) { }}) } ctl := dbmodel.Controller{ - UUID: s.ControllerConfig.ControllerUUID(), - Name: s.ControllerConfig.ControllerName(), - CACertificate: info.CACert, - AdminUser: info.Tag.Id(), - AdminPassword: info.Password, - Addresses: hpss, + UUID: s.ControllerConfig.ControllerUUID(), + Name: s.ControllerConfig.ControllerName(), + CACertificate: info.CACert, + AdminIdentityName: info.Tag.Id(), + AdminPassword: info.Password, + Addresses: hpss, } s.API, err = s.Dialer.Dial(context.Background(), &ctl, s.Model.ModelTag(), nil) diff --git a/internal/jujuclient/ping_test.go b/internal/jujuclient/ping_test.go index 8e80b5cdb..54ab09c61 100644 --- a/internal/jujuclient/ping_test.go +++ b/internal/jujuclient/ping_test.go @@ -21,12 +21,12 @@ func (s *pingSuite) TestPing(c *gc.C) { info := s.APIInfo(c) ctl := dbmodel.Controller{ - UUID: s.ControllerConfig.ControllerUUID(), - Name: s.ControllerConfig.ControllerName(), - CACertificate: info.CACert, - AdminUser: info.Tag.Id(), - AdminPassword: info.Password, - PublicAddress: info.Addrs[0], + UUID: s.ControllerConfig.ControllerUUID(), + Name: s.ControllerConfig.ControllerName(), + CACertificate: info.CACert, + AdminIdentityName: info.Tag.Id(), + AdminPassword: info.Password, + PublicAddress: info.Addrs[0], } api, err := s.Dialer.Dial(ctx, &ctl, names.ModelTag{}, nil) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuclient/storage_test.go b/internal/jujuclient/storage_test.go index 20ec0a983..6f449bc64 100644 --- a/internal/jujuclient/storage_test.go +++ b/internal/jujuclient/storage_test.go @@ -35,12 +35,12 @@ func (s *storageSuite) TestListFilesystems(c *gc.C) { info := s.APIInfo(c) ctl := dbmodel.Controller{ - UUID: s.ControllerConfig.ControllerUUID(), - Name: s.ControllerConfig.ControllerName(), - CACertificate: info.CACert, - AdminUser: info.Tag.Id(), - AdminPassword: info.Password, - PublicAddress: info.Addrs[0], + UUID: s.ControllerConfig.ControllerUUID(), + Name: s.ControllerConfig.ControllerName(), + CACertificate: info.CACert, + AdminIdentityName: info.Tag.Id(), + AdminPassword: info.Password, + PublicAddress: info.Addrs[0], } models, err := s.API.UpdateCredential(ctx, cred) @@ -81,12 +81,12 @@ func (s *storageSuite) TestListVolumes(c *gc.C) { info := s.APIInfo(c) ctl := dbmodel.Controller{ - UUID: s.ControllerConfig.ControllerUUID(), - Name: s.ControllerConfig.ControllerName(), - CACertificate: info.CACert, - AdminUser: info.Tag.Id(), - AdminPassword: info.Password, - PublicAddress: info.Addrs[0], + UUID: s.ControllerConfig.ControllerUUID(), + Name: s.ControllerConfig.ControllerName(), + CACertificate: info.CACert, + AdminIdentityName: info.Tag.Id(), + AdminPassword: info.Password, + PublicAddress: info.Addrs[0], } models, err := s.API.UpdateCredential(ctx, cred) @@ -127,12 +127,12 @@ func (s *storageSuite) TestListStorageDetails(c *gc.C) { info := s.APIInfo(c) ctl := dbmodel.Controller{ - UUID: s.ControllerConfig.ControllerUUID(), - Name: s.ControllerConfig.ControllerName(), - CACertificate: info.CACert, - AdminUser: info.Tag.Id(), - AdminPassword: info.Password, - PublicAddress: info.Addrs[0], + UUID: s.ControllerConfig.ControllerUUID(), + Name: s.ControllerConfig.ControllerName(), + CACertificate: info.CACert, + AdminIdentityName: info.Tag.Id(), + AdminPassword: info.Password, + PublicAddress: info.Addrs[0], } models, err := s.API.UpdateCredential(ctx, cred) From ce12615747a749cad9ec410c9904e6e225b0d7ce Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 17 Jan 2024 15:05:29 +0000 Subject: [PATCH 039/126] Rename `dbmodel.User` reference to `dbmodel.Identity` Signed-off-by: Babak K. Shandiz --- internal/jimm/service_account_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/jimm/service_account_test.go b/internal/jimm/service_account_test.go index c86103e4e..924696d60 100644 --- a/internal/jimm/service_account_test.go +++ b/internal/jimm/service_account_test.go @@ -25,8 +25,8 @@ func TestAddServiceAccount(t *testing.T) { } c.Assert(err, qt.IsNil) user := openfga.NewUser( - &dbmodel.User{ - Username: "bob@external", + &dbmodel.Identity{ + Name: "bob@external", DisplayName: "Bob", }, client, @@ -37,8 +37,8 @@ func TestAddServiceAccount(t *testing.T) { err = j.AddServiceAccount(ctx, user, clientID) c.Assert(err, qt.IsNil) userAlice := openfga.NewUser( - &dbmodel.User{ - Username: "alive@external", + &dbmodel.Identity{ + Name: "alive@external", DisplayName: "Alice", }, client, From 732a120a086f891a4aca63d546bc8e28d76bc589 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Wed, 17 Jan 2024 15:14:50 -0800 Subject: [PATCH 040/126] CSS-6764 update cloud creds (#1134) * Introduced UpdateServiceAccountCredentials method * Adding tests * Fix tests * PR comments * PR comment * Rename user to identity * Minor fixes --- api/params/params.go | 8 + internal/jimm/cloudcredential.go | 7 + internal/jimm/service_account.go | 2 +- internal/jimmtest/jimm_mock.go | 27 +-- internal/jujuapi/cloud.go | 18 +- internal/jujuapi/export_test.go | 7 + internal/jujuapi/jimm.go | 2 + internal/jujuapi/service_account.go | 68 ++++++++ internal/jujuapi/service_account_test.go | 209 ++++++++++++++++++++++- pkg/names/service_account.go | 2 +- 10 files changed, 330 insertions(+), 20 deletions(-) diff --git a/api/params/params.go b/api/params/params.go index fc7bbced7..9e9af0eb2 100644 --- a/api/params/params.go +++ b/api/params/params.go @@ -401,3 +401,11 @@ type AddServiceAccountRequest struct { // ClientID holds the client id of the service account. ClientID string `json:"client-id"` } + +// UpdateServiceAccountCredentialsRequest holds a request to update +// a service accounts cloud credentials. +type UpdateServiceAccountCredentialsRequest struct { + jujuparams.UpdateCredentialArgs + // ClientID holds the client id of the service account. + ClientID string `json:"client-id"` +} diff --git a/internal/jimm/cloudcredential.go b/internal/jimm/cloudcredential.go index 9e28394ac..eb122c671 100644 --- a/internal/jimm/cloudcredential.go +++ b/internal/jimm/cloudcredential.go @@ -155,6 +155,13 @@ func (j *JIMM) UpdateCloudCredential(ctx context.Context, user *openfga.User, ar return result, errors.E(op, err) } + // Confirm the cloud exists. + var cloud dbmodel.Cloud + cloud.SetTag(names.NewCloudTag(credential.CloudName)) + if err = j.Database.GetCloud(ctx, &cloud); err != nil { + return result, errors.E(op, err) + } + models, err := j.Database.GetModelsUsingCredential(ctx, credential.ID) if err != nil { return result, errors.E(op, err) diff --git a/internal/jimm/service_account.go b/internal/jimm/service_account.go index 52d803b88..4a2bce026 100644 --- a/internal/jimm/service_account.go +++ b/internal/jimm/service_account.go @@ -45,7 +45,7 @@ func (j *JIMM) AddServiceAccount(ctx context.Context, u *openfga.User, clientId Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(svcTag), } - err = j.AuthorizationClient().AddRelation(ctx, addTuple) + err = j.OpenFGAClient.AddRelation(ctx, addTuple) if err != nil { return errors.E(op, err) } diff --git a/internal/jimmtest/jimm_mock.go b/internal/jimmtest/jimm_mock.go index d8c415107..f5587c0d9 100644 --- a/internal/jimmtest/jimm_mock.go +++ b/internal/jimmtest/jimm_mock.go @@ -17,7 +17,6 @@ import ( "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/jimm" - "github.com/canonical/jimm/internal/jujuapi" "github.com/canonical/jimm/internal/openfga" "github.com/canonical/jimm/internal/pubsub" ) @@ -27,8 +26,6 @@ import ( // will delegate to the requested funcion or if the funcion is nil return // a NotImplemented error. type JIMM struct { - jujuapi.JIMM - AddAuditLogEntry_ func(ale *dbmodel.AuditLogEntry) AddCloudToController_ func(ctx context.Context, user *openfga.User, controllerName string, tag names.CloudTag, cloud jujuparams.Cloud, force bool) error AddController_ func(ctx context.Context, u *openfga.User, ctl *dbmodel.Controller) error @@ -68,7 +65,9 @@ type JIMM struct { GrantModelAccess_ func(ctx context.Context, user *openfga.User, mt names.ModelTag, ut names.UserTag, access jujuparams.UserAccessPermission) error GrantOfferAccess_ func(ctx context.Context, u *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) error ImportModel_ func(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error + IdentityModelDefaults_ func(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) InitiateMigration_ func(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec, targetControllerID uint) (jujuparams.InitiateMigrationResult, error) + InitiateInternalMigration_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetController string) (jujuparams.InitiateMigrationResult, error) ListApplicationOffers_ func(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) ListControllers_ func(ctx context.Context, user *openfga.User) ([]dbmodel.Controller, error) ModelDefaultsForCloud_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) @@ -90,13 +89,13 @@ type JIMM struct { SetControllerConfig_ func(ctx context.Context, u *openfga.User, args jujuparams.ControllerConfigSet) error SetControllerDeprecated_ func(ctx context.Context, user *openfga.User, controllerName string, deprecated bool) error SetModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, configs map[string]interface{}) error - SetUserModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error + SetIdentityModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error UnsetModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error UpdateApplicationOffer_ func(ctx context.Context, controller *dbmodel.Controller, offerUUID string, removed bool) error UpdateCloud_ func(ctx context.Context, u *openfga.User, ct names.CloudTag, cloud jujuparams.Cloud) error UpdateCloudCredential_ func(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) UpdateMigratedModel_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetControllerName string) error - UserModelDefaults_ func(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) + UpdateServiceAccountCredentials_ func() ValidateModelUpgrade_ func(ctx context.Context, u *openfga.User, mt names.ModelTag, force bool) error WatchAllModelSummaries_ func(ctx context.Context, controller *dbmodel.Controller) (_ func() error, err error) } @@ -343,6 +342,12 @@ func (j *JIMM) InitiateMigration(ctx context.Context, user *openfga.User, spec j } return j.InitiateMigration_(ctx, user, spec, targetControllerID) } +func (j *JIMM) InitiateInternalMigration(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetController string) (jujuparams.InitiateMigrationResult, error) { + if j.InitiateInternalMigration_ == nil { + return jujuparams.InitiateMigrationResult{}, errors.E(errors.CodeNotImplemented) + } + return j.InitiateInternalMigration_(ctx, user, modelTag, targetController) +} func (j *JIMM) ListApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) { if j.ListApplicationOffers_ == nil { return nil, errors.E(errors.CodeNotImplemented) @@ -469,11 +474,11 @@ func (j *JIMM) SetModelDefaults(ctx context.Context, user *dbmodel.Identity, clo } return j.SetModelDefaults_(ctx, user, cloudTag, region, configs) } -func (j *JIMM) SetUserModelDefaults(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error { - if j.SetUserModelDefaults_ == nil { +func (j *JIMM) SetIdentityModelDefaults(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error { + if j.SetIdentityModelDefaults_ == nil { return errors.E(errors.CodeNotImplemented) } - return j.SetUserModelDefaults_(ctx, user, configs) + return j.SetIdentityModelDefaults_(ctx, user, configs) } func (j *JIMM) UnsetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error { if j.UnsetModelDefaults_ == nil { @@ -505,11 +510,11 @@ func (j *JIMM) UpdateMigratedModel(ctx context.Context, user *openfga.User, mode } return j.UpdateMigratedModel_(ctx, user, modelTag, targetControllerName) } -func (j *JIMM) UserModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) { - if j.UserModelDefaults_ == nil { +func (j *JIMM) IdentityModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) { + if j.IdentityModelDefaults_ == nil { return nil, errors.E(errors.CodeNotImplemented) } - return j.UserModelDefaults_(ctx, user) + return j.IdentityModelDefaults_(ctx, user) } func (j *JIMM) ValidateModelUpgrade(ctx context.Context, u *openfga.User, mt names.ModelTag, force bool) error { if j.ValidateModelUpgrade_ == nil { diff --git a/internal/jujuapi/cloud.go b/internal/jujuapi/cloud.go index 225a0f9b8..40a00841d 100644 --- a/internal/jujuapi/cloud.go +++ b/internal/jujuapi/cloud.go @@ -237,15 +237,21 @@ func (r *controllerRoot) AddCredentials(ctx context.Context, args jujuparams.Tag if err != nil { return jujuparams.ErrorResults{}, errors.E(op, err) } + results := collapseUpdateCredentialResults(args, updateResults) + return results, nil +} + +// collapseUpdateCredentialResults returns a combined set of error results. +// If there are any models that are using a credential and these models +// are not going to be visible with updated credential content, +// there will be detailed validation errors per model. +// However, old return parameter structure could not hold this much detail and, +// thus, per-model-per-credential errors are squashed into per-credential errors. +func collapseUpdateCredentialResults(args jujuparams.TaggedCredentials, updateResults jujuparams.UpdateCredentialResults) jujuparams.ErrorResults { results := jujuparams.ErrorResults{ Results: make([]jujuparams.ErrorResult, len(args.Credentials)), } - // If there are any models that are using a credential and these models - // are not going to be visible with updated credential content, - // there will be detailed validation errors per model. - // However, old return parameter structure could not hold this much detail and, - // thus, per-model-per-credential errors are squashed into per-credential errors. for i, result := range updateResults.Results { var resultErrors []jujuparams.ErrorResult if result.Error != nil { @@ -267,7 +273,7 @@ func (r *controllerRoot) AddCredentials(ctx context.Context, args jujuparams.Tag results.Results[i].Error = apiservererrors.ServerError(credentialError.Combine()) } } - return results, nil + return results } func userModelAccess(ctx context.Context, user *openfga.User, model names.ModelTag) (string, error) { diff --git a/internal/jujuapi/export_test.go b/internal/jujuapi/export_test.go index 429e326fa..dcc4c2d43 100644 --- a/internal/jujuapi/export_test.go +++ b/internal/jujuapi/export_test.go @@ -7,6 +7,7 @@ import ( "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/jimm" + "github.com/canonical/jimm/internal/openfga" ofganames "github.com/canonical/jimm/internal/openfga/names" jujuparams "github.com/juju/juju/rpc/params" ) @@ -51,3 +52,9 @@ func ToJAASTag(db db.Database, tag *ofganames.Tag) (string, error) { func NewControllerRoot(j JIMM, p Params) *controllerRoot { return newControllerRoot(j, p) } + +var SetUser = func(r *controllerRoot, u *openfga.User) { + r.mu.Lock() + r.user = u + r.mu.Unlock() +} diff --git a/internal/jujuapi/jimm.go b/internal/jujuapi/jimm.go index 3b8b30f37..b6be9d368 100644 --- a/internal/jujuapi/jimm.go +++ b/internal/jujuapi/jimm.go @@ -50,6 +50,7 @@ func init() { purgeLogsMethod := rpc.Method(r.PurgeLogs) migrateModel := rpc.Method(r.MigrateModel) addServiceAccountMethod := rpc.Method(r.AddServiceAccount) + updateServiceAccountCredentials := rpc.Method(r.UpdateServiceAccountCredentials) // JIMM Generic RPC r.AddMethod("JIMM", 4, "AddController", addControllerMethod) @@ -80,6 +81,7 @@ func init() { r.AddMethod("JIMM", 4, "CrossModelQuery", crossModelQueryMethod) // JIMM Service Accounts r.AddMethod("JIMM", 4, "AddServiceAccount", addServiceAccountMethod) + r.AddMethod("JIMM", 4, "UpdateServiceAccountCredentials", updateServiceAccountCredentials) return []int{4} } diff --git a/internal/jujuapi/service_account.go b/internal/jujuapi/service_account.go index 2f3921c23..e9ecc1b6c 100644 --- a/internal/jujuapi/service_account.go +++ b/internal/jujuapi/service_account.go @@ -5,8 +5,15 @@ package jujuapi import ( "context" + jujuparams "github.com/juju/juju/rpc/params" + "github.com/juju/names/v4" + apiparams "github.com/canonical/jimm/api/params" + "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" + "github.com/canonical/jimm/internal/jimm" + "github.com/canonical/jimm/internal/openfga" + ofganames "github.com/canonical/jimm/internal/openfga/names" jimmnames "github.com/canonical/jimm/pkg/names" ) @@ -22,3 +29,64 @@ func (r *controllerRoot) AddServiceAccount(ctx context.Context, req apiparams.Ad return r.jimm.AddServiceAccount(ctx, r.user, req.ClientID) } + +// UpdateServiceAccountCredentialsCheckModels updates a set of cloud credentials' content. +// If there are any models that are using a credential and these models +// are not going to be visible with updated credential content, +// there will be detailed validation errors per model. +// +// This method checks that the authenticated user has permission to manage the service account. +func (r *controllerRoot) UpdateServiceAccountCredentials(ctx context.Context, req apiparams.UpdateServiceAccountCredentialsRequest) (jujuparams.UpdateCredentialResults, error) { + const op = errors.Op("jujuapi.UpdateServiceAccountCredentials") + + if !jimmnames.IsValidServiceAccountId(req.ClientID) { + return jujuparams.UpdateCredentialResults{}, errors.E(op, errors.CodeBadRequest, "invalid client ID") + } + + tuple := openfga.Tuple{ + Object: ofganames.ConvertTag(r.user.ResourceTag()), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(req.ClientID)), + } + ok, err := r.jimm.AuthorizationClient().CheckRelation(ctx, tuple, false) + if err != nil { + return jujuparams.UpdateCredentialResults{}, errors.E(op, errors.CodeOpenFGARequestFailed, "unable to determine permissions") + } + if !ok { + return jujuparams.UpdateCredentialResults{}, errors.E(op, errors.CodeUnauthorized, "unauthorized") + } + + var targetUserModel dbmodel.Identity + targetUserModel.SetTag(names.NewUserTag(req.ClientID)) + if err := r.jimm.DB().GetIdentity(ctx, &targetUserModel); err != nil { + return jujuparams.UpdateCredentialResults{}, errors.E(op, err) + } + targetUser := openfga.NewUser(&targetUserModel, r.jimm.AuthorizationClient()) + + results := jujuparams.UpdateCredentialResults{ + Results: make([]jujuparams.UpdateCredentialResult, len(req.Credentials)), + } + for i, credential := range req.Credentials { + var res []jujuparams.UpdateCredentialModelResult + var err error + var tag names.CloudCredentialTag + tag, err = names.ParseCloudCredentialTag(credential.Tag) + if err == nil { + res, err = r.jimm.UpdateCloudCredential(ctx, targetUser, jimm.UpdateCloudCredentialArgs{ + CredentialTag: tag, + Credential: credential.Credential, + // Check that all credentials are valid. + SkipCheck: false, + // Update all credentials on target controllers. + SkipUpdate: false, + }) + } + results.Results[i] = jujuparams.UpdateCredentialResult{ + CredentialTag: credential.Tag, + Error: mapError(err), + Models: res, + } + results.Results[i].CredentialTag = credential.Tag + } + return results, nil +} diff --git a/internal/jujuapi/service_account_test.go b/internal/jujuapi/service_account_test.go index 8b472ae77..e9bd1b339 100644 --- a/internal/jujuapi/service_account_test.go +++ b/internal/jujuapi/service_account_test.go @@ -6,13 +6,24 @@ import ( "context" "testing" + qt "github.com/frankban/quicktest" + jujuparams "github.com/juju/juju/rpc/params" + "github.com/juju/names/v4" + gc "gopkg.in/check.v1" + "github.com/canonical/jimm/api/params" + "github.com/canonical/jimm/internal/db" + "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/jimmtest" "github.com/canonical/jimm/internal/jujuapi" "github.com/canonical/jimm/internal/openfga" - qt "github.com/frankban/quicktest" + ofganames "github.com/canonical/jimm/internal/openfga/names" + jimmnames "github.com/canonical/jimm/pkg/names" ) +// Unit tests (see below for integration tests). + func TestAddServiceAccount(t *testing.T) { c := qt.New(t) @@ -57,3 +68,199 @@ func TestAddServiceAccount(t *testing.T) { }) } } + +func TestUpdateServiceAccountCredentials(t *testing.T) { + c := qt.New(t) + + tests := []struct { + about string + updateCloudCredential func(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) + args params.UpdateServiceAccountCredentialsRequest + username string + addTuples []openfga.Tuple + expectedResult jujuparams.UpdateCredentialResults + expectedError string + }{{ + about: "Valid request", + updateCloudCredential: func(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) { + return nil, nil + }, + expectedResult: jujuparams.UpdateCredentialResults{ + Results: []jujuparams.UpdateCredentialResult{ + { + CredentialTag: "cloudcred-aws/1cbe5066-ea80-4979-8633-048d32f46cf8/cred-name", + Error: nil, + Models: nil, + }, + { + CredentialTag: "cloudcred-azure/1cbe5066-ea80-4979-8633-048d32f46cf8/cred-name2", + Error: nil, + Models: nil, + }, + }}, + args: params.UpdateServiceAccountCredentialsRequest{ + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ + Credentials: []jujuparams.TaggedCredential{ + { + Tag: "cloudcred-aws/1cbe5066-ea80-4979-8633-048d32f46cf8/cred-name", + Credential: jujuparams.CloudCredential{Attributes: map[string]string{"foo": "bar"}}, + }, + { + Tag: "cloudcred-azure/1cbe5066-ea80-4979-8633-048d32f46cf8/cred-name2", + Credential: jujuparams.CloudCredential{Attributes: map[string]string{"wolf": "low"}}, + }, + }}, + }, + username: "alice", + addTuples: []openfga.Tuple{{ + Object: ofganames.ConvertTag(names.NewUserTag("alice")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), + }}, + }, { + about: "Invalid Credential Tag", + updateCloudCredential: func(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) { + return nil, nil + }, + expectedResult: jujuparams.UpdateCredentialResults{ + Results: []jujuparams.UpdateCredentialResult{ + { + CredentialTag: "invalid-cred-name", + Error: &jujuparams.Error{ + Message: `"invalid-cred-name" is not a valid tag`, + }, + Models: nil, + }, + }}, + args: params.UpdateServiceAccountCredentialsRequest{ + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ + Credentials: []jujuparams.TaggedCredential{ + { + Tag: "invalid-cred-name", + Credential: jujuparams.CloudCredential{Attributes: map[string]string{"foo": "bar"}}, + }, + }}, + }, + username: "alice", + addTuples: []openfga.Tuple{{ + Object: ofganames.ConvertTag(names.NewUserTag("alice")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), + }}, + }, { + about: "Missing service account administrator permission", + updateCloudCredential: func(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) { + return nil, nil + }, + expectedError: "unauthorized", + args: params.UpdateServiceAccountCredentialsRequest{ + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + }, + username: "alice", + }, { + about: "Invalid Client ID", + updateCloudCredential: func(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) { + return nil, nil + }, + username: "alice", + args: params.UpdateServiceAccountCredentialsRequest{ + ClientID: "_123_", + }, + expectedError: "invalid client ID", + }} + + for _, test := range tests { + test := test + c.Run(test.about, func(c *qt.C) { + ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) + c.Assert(err, qt.IsNil) + pgDb := db.Database{ + DB: jimmtest.PostgresDB(c, nil), + } + err = pgDb.Migrate(context.Background(), false) + c.Assert(err, qt.IsNil) + jimm := &jimmtest.JIMM{ + AuthorizationClient_: func() *openfga.OFGAClient { return ofgaClient }, + UpdateCloudCredential_: test.updateCloudCredential, + DB_: func() *db.Database { return &pgDb }, + } + var u dbmodel.Identity + u.SetTag(names.NewUserTag(test.username)) + user := openfga.NewUser(&u, ofgaClient) + cr := jujuapi.NewControllerRoot(jimm, jujuapi.Params{}) + jujuapi.SetUser(cr, user) + + if len(test.addTuples) > 0 { + ofgaClient.AddRelation(context.Background(), test.addTuples...) + } + + res, err := cr.UpdateServiceAccountCredentials(context.Background(), test.args) + if test.expectedError == "" { + c.Assert(err, qt.IsNil) + c.Assert(res, qt.DeepEquals, test.expectedResult) + } else { + c.Assert(err, qt.ErrorMatches, test.expectedError) + } + }) + } +} + +// Integration tests below. +type serviceAccountSuite struct { + websocketSuite +} + +var _ = gc.Suite(&serviceAccountSuite{}) + +func (s *serviceAccountSuite) TestUpdateServiceAccountCredentialsIntegration(c *gc.C) { + conn := s.open(c, nil, "bob") + defer conn.Close() + + serviceAccount := jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be") + + tuple := openfga.Tuple{ + Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(serviceAccount), + } + + s.JIMM.OpenFGAClient.AddRelation(context.Background(), tuple) + cloud := &dbmodel.Cloud{ + Name: "aws", + } + s.JIMM.Database.AddCloud(context.Background(), cloud) + + var credResults jujuparams.UpdateCredentialResults + err := conn.APICall("JIMM", 4, "", "UpdateServiceAccountCredentials", params.UpdateServiceAccountCredentialsRequest{ + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ + Credentials: []jujuparams.TaggedCredential{ + { + Tag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be/cred-name", + Credential: jujuparams.CloudCredential{Attributes: map[string]string{"foo": "bar"}}, + }, + { + Tag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be/cred-name2", + Credential: jujuparams.CloudCredential{Attributes: map[string]string{"wolf": "low"}}, + }, + }}, + }, &credResults) + + expectedResult := jujuparams.UpdateCredentialResults{ + Results: []jujuparams.UpdateCredentialResult{ + { + CredentialTag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be/cred-name", + Error: nil, + Models: nil, + }, + { + CredentialTag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be/cred-name2", + Error: nil, + Models: nil, + }, + }} + c.Assert(err, gc.Equals, nil) + c.Assert(credResults, gc.DeepEquals, expectedResult) +} diff --git a/pkg/names/service_account.go b/pkg/names/service_account.go index 4e851b38d..585d62864 100644 --- a/pkg/names/service_account.go +++ b/pkg/names/service_account.go @@ -40,7 +40,7 @@ func (t ServiceAccountTag) String() string { return ServiceAccountTagKind + "-" func NewServiceAccountTag(clientId string) ServiceAccountTag { id := validClientId.FindString(clientId) - if id == "" { + if !IsValidServiceAccountId(clientId) { panic(fmt.Sprintf("invalid client tag %q", clientId)) } From c14763190101bb3cbc916960c981adc65e5cc38c Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Thu, 18 Jan 2024 21:40:03 +0000 Subject: [PATCH 041/126] =?UTF-8?q?feat(auth=20service=20device=20flow):?= =?UTF-8?q?=20introduces=20an=20auth=20service=20capable=20of=E2=80=A6=20(?= =?UTF-8?q?#1137)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(auth service device flow): introduces an auth service capable of performing oauth2 device flow To isolate the oidc and oauth2 logic, a new service has been created with a single flow currently (device) and a means to verify the id token, and additionally extract the email. It is currently uncertain whether we should ensure the email is bverificed first. The test is very much an all-in-one test as it semantically makes sense to do the entire flow in a single test for the device flow. * feat(gomod and local env): gomod and local env I forgotten to commit this. * feat(pr commentds): pr comments --- go.mod | 6 +- go.sum | 7 +- internal/auth/oauth2.go | 137 + internal/auth/oauth2_test.go | 111 + local/keycloak/jimm-realm.json | 4259 ++++++++++++++++---------------- 5 files changed, 2389 insertions(+), 2131 deletions(-) create mode 100644 internal/auth/oauth2.go create mode 100644 internal/auth/oauth2_test.go diff --git a/go.mod b/go.mod index 3a926934b..e87104107 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( require ( github.com/canonical/ofga v0.10.0 + github.com/coreos/go-oidc/v3 v3.9.0 github.com/dustinkirkland/golang-petname v0.0.0-20230626224747-e794b9370d49 github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/render v1.0.2 @@ -56,6 +57,7 @@ require ( github.com/lestrrat-go/jwx/v2 v2.0.11 github.com/oklog/ulid/v2 v2.1.0 github.com/stretchr/testify v1.8.4 + golang.org/x/oauth2 v0.13.0 gopkg.in/errgo.v1 v1.0.1 gopkg.in/httprequest.v1 v1.2.1 gopkg.in/yaml.v2 v2.4.0 @@ -129,6 +131,7 @@ require ( github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/tcell/v2 v2.5.1 // indirect github.com/go-goose/goose/v5 v5.0.0-20230421180421-abaee9096e3a // indirect + github.com/go-jose/go-jose/v3 v3.0.1 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect @@ -340,14 +343,13 @@ require ( go.uber.org/multierr v1.8.0 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/mod v0.13.0 // indirect - golang.org/x/oauth2 v0.13.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.14.0 // indirect google.golang.org/api v0.126.0 // indirect - google.golang.org/appengine v1.6.7 // indirect + google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect diff --git a/go.sum b/go.sum index 9d478021f..9d51f21f0 100644 --- a/go.sum +++ b/go.sum @@ -439,6 +439,8 @@ github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= +github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -601,6 +603,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 github.com/go-goose/goose/v5 v5.0.0-20230421180421-abaee9096e3a h1:H/l82+fC6idmYg1kfpQlCq7gYctri7AGn9RemqwN6bw= github.com/go-goose/goose/v5 v5.0.0-20230421180421-abaee9096e3a/go.mod h1:BxICmnmP7QlxZhKP2BHkpWQS0tbb3LrsrLtd9TQyyms= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= @@ -2438,8 +2442,9 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go new file mode 100644 index 000000000..d11883f1a --- /dev/null +++ b/internal/auth/oauth2.go @@ -0,0 +1,137 @@ +// Copyright 2024 canonical. + +package auth + +import ( + "context" + + "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" + + "github.com/canonical/jimm/internal/errors" +) + +// AuthenticationService handles authentication within JIMM. +type AuthenticationService struct { + deviceConfig oauth2.Config + // provider holds a OIDC provider wrapper for the OAuth2.0 /x/oauth package, + // enabling UserInfo calls, wellknown retrieval and jwks verification. + provider *oidc.Provider +} + +// AuthenticationServiceParams holds the parameters to initialise +// an Authentication Service. +type AuthenticationServiceParams struct { + // IssuerURL is the URL of the OAuth2.0 server. + // I.e., http://localhost:8082/realms/jimm in the case of keycloak. + IssuerURL string + // DeviceClientID holds the OAuth2.0 client id registered and configured + // to handle device OAuth2.0 flows. The client is NOT expected to be confidential + // and as such does not need a client secret (given it is configured correctly). + DeviceClientID string + // DeviceScopes holds the scopes that you wish to retrieve. + DeviceScopes []string +} + +// NewAuthenticationService returns a new authentication service for handling +// authentication within JIMM. +func NewAuthenticationService(ctx context.Context, params AuthenticationServiceParams) (*AuthenticationService, error) { + const op = errors.Op("auth.NewAuthenticationService") + + provider, err := oidc.NewProvider(ctx, params.IssuerURL) + if err != nil { + return nil, errors.E(op, err, "failed to create oidc provider") + } + + return &AuthenticationService{ + provider: provider, + deviceConfig: oauth2.Config{ + ClientID: params.DeviceClientID, + Endpoint: provider.Endpoint(), + Scopes: params.DeviceScopes, + }, + }, nil +} + +// Device initiates a device flow login and is step ONE of TWO. +// +// This is done via retrieving a: +// - Device code +// - User code +// - VerificationURI +// - Interval +// - Expiry +// From the device /auth endpoint. +// +// The verification uri and user code is sent to the user, as they must enter the code +// into the uri. +// +// The interval, expiry and device code and used to poll the token endpoint for completion. +func (as *AuthenticationService) Device(ctx context.Context) (*oauth2.DeviceAuthResponse, error) { + const op = errors.Op("auth.AuthenticationService.Device") + + resp, err := as.deviceConfig.DeviceAuth(ctx) + if err != nil { + return nil, errors.E(op, err, "device auth call failed") + } + + return resp, nil +} + +// DeviceAccessToken continues and collect an access token during the device login flow +// and is step TWO. +// +// See Device(...) godoc for more info pertaining to the fko. +func (as *AuthenticationService) DeviceAccessToken(ctx context.Context, res *oauth2.DeviceAuthResponse) (*oauth2.Token, error) { + const op = errors.Op("auth.AuthenticationService.DeviceAccessToken") + + t, err := as.deviceConfig.DeviceAccessToken(ctx, res) + if err != nil { + return nil, errors.E(op, err, "device access token call failed") + } + + return t, nil +} + +// ExtractAndVerifyIDToken extracts the id token from the extras claims of an oauth2 token +// and performs signature verification of the token. +func (as *AuthenticationService) ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) { + const op = errors.Op("auth.AuthenticationService.ExtractAndVerifyIDToken") + + // Extract the ID Token from oauth2 token. + rawIDToken, ok := oauth2Token.Extra("id_token").(string) + if !ok { + return nil, errors.E(op, "failed to extract id token") + } + + verifier := as.provider.Verifier(&oidc.Config{ + ClientID: as.deviceConfig.ClientID, + }) + + token, err := verifier.Verify(ctx, rawIDToken) + if err != nil { + return nil, errors.E(op, err, "failed to verify id token") + } + + return token, nil +} + +// Email retrieves the users email from an id token via the email claim +func (as *AuthenticationService) Email(idToken *oidc.IDToken) (string, error) { + const op = errors.Op("auth.AuthenticationService.Email") + + var claims struct { + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` // TODO(ale8k): Add verification logic + } + if idToken == nil { + return "", errors.E(op, "id token is nil") + } + + if err := idToken.Claims(&claims); err != nil { + return "", errors.E(op, err, "failed to extract claims") + } + + return claims.Email, nil + +} diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go new file mode 100644 index 000000000..82a2d6866 --- /dev/null +++ b/internal/auth/oauth2_test.go @@ -0,0 +1,111 @@ +// Copyright 2024 canonical. + +package auth_test + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/cookiejar" + "net/url" + "regexp" + "testing" + + "github.com/canonical/jimm/internal/auth" + "github.com/coreos/go-oidc/v3/oidc" + qt "github.com/frankban/quicktest" +) + +// TestDevice is a unique test in that it runs through the entire device oauth2.0 +// flow and additionally ensures the id token is verified and correct. +// +// This test requires the local docker compose to be running and keycloak +// to be available. +// +// Some calls perform regexes against the response HTML forms such that we +// can manually POST the forms throughout the flow. +func TestDevice(t *testing.T) { + c := qt.New(t) + + ctx := context.Background() + + authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + }) + c.Assert(err, qt.IsNil) + + res, err := authSvc.Device(ctx) + c.Assert(err, qt.IsNil) + + jar, err := cookiejar.New(nil) + c.Assert(err, qt.IsNil) + + client := &http.Client{ + Jar: jar, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + fmt.Println("redirected to", req.URL) + return nil + }, + } + + // Post login form + verResp, err := client.Get(res.VerificationURIComplete) + c.Assert(err, qt.IsNil) + defer verResp.Body.Close() + b, err := io.ReadAll(verResp.Body) + c.Assert(err, qt.IsNil) + + re := regexp.MustCompile(`action="(.*?)" method=`) + match := re.FindStringSubmatch(string(b)) + loginFormUrl := match[1] + + v := url.Values{} + // The username and password are hardcoded witih jimm-realm.json in our local + // keycloak configuration for the jimm realm. + v.Add("username", "jimm-test") + v.Add("password", "password") + loginResp, err := client.PostForm(loginFormUrl, v) + c.Assert(err, qt.IsNil) + defer loginResp.Body.Close() + + // Post consent + b, err = io.ReadAll(loginResp.Body) + c.Assert(err, qt.IsNil) + + re = regexp.MustCompile(`action="(.*?)" method=`) + match = re.FindStringSubmatch(string(b)) + consentFormUri := match[1] + v = url.Values{} + v.Add("accept", "Yes") + consentResp, err := client.PostForm("http://localhost:8082"+consentFormUri, v) + c.Assert(err, qt.IsNil) + defer consentResp.Body.Close() + + // Read consent resp + b, err = io.ReadAll(consentResp.Body) + c.Assert(err, qt.IsNil) + + re = regexp.MustCompile(`Device Login Successful`) + c.Assert(re.MatchString(string(b)), qt.IsTrue) + + // Retrieve access token + token, err := authSvc.DeviceAccessToken(ctx, res) + c.Assert(err, qt.IsNil) + c.Assert(token, qt.IsNotNil) + + // Extract and verify id token + idToken, err := authSvc.ExtractAndVerifyIDToken(ctx, token) + c.Assert(err, qt.IsNil) + c.Assert(idToken, qt.IsNotNil) + + // Test subject set + c.Assert(idToken.Subject, qt.Equals, "8281cec3-5b48-46eb-a41d-72c15ec3f9e0") + + // Retrieve the email + email, err := authSvc.Email(idToken) + c.Assert(err, qt.IsNil) + c.Assert(email, qt.Equals, "jimm-test@canonical.com") +} diff --git a/local/keycloak/jimm-realm.json b/local/keycloak/jimm-realm.json index edae7b871..c5ee839fb 100644 --- a/local/keycloak/jimm-realm.json +++ b/local/keycloak/jimm-realm.json @@ -1,2261 +1,2264 @@ { - "id": "b307f672-e789-4f4d-bb54-051f67686046", - "realm": "jimm", - "notBefore": 0, - "defaultSignatureAlgorithm": "RS256", - "revokeRefreshToken": false, - "refreshTokenMaxReuse": 0, - "accessTokenLifespan": 300, - "accessTokenLifespanForImplicitFlow": 900, - "ssoSessionIdleTimeout": 1800, - "ssoSessionMaxLifespan": 36000, - "ssoSessionIdleTimeoutRememberMe": 0, - "ssoSessionMaxLifespanRememberMe": 0, - "offlineSessionIdleTimeout": 2592000, - "offlineSessionMaxLifespanEnabled": false, - "offlineSessionMaxLifespan": 5184000, - "clientSessionIdleTimeout": 0, - "clientSessionMaxLifespan": 0, - "clientOfflineSessionIdleTimeout": 0, - "clientOfflineSessionMaxLifespan": 0, - "accessCodeLifespan": 60, - "accessCodeLifespanUserAction": 300, - "accessCodeLifespanLogin": 1800, - "actionTokenGeneratedByAdminLifespan": 43200, - "actionTokenGeneratedByUserLifespan": 300, - "oauth2DeviceCodeLifespan": 600, - "oauth2DevicePollingInterval": 5, - "enabled": true, - "sslRequired": "external", - "registrationAllowed": false, - "registrationEmailAsUsername": false, - "rememberMe": false, - "verifyEmail": false, - "loginWithEmailAllowed": true, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": false, - "editUsernameAllowed": false, - "bruteForceProtected": false, - "permanentLockout": false, - "maxFailureWaitSeconds": 900, - "minimumQuickLoginWaitSeconds": 60, - "waitIncrementSeconds": 60, - "quickLoginCheckMilliSeconds": 1000, - "maxDeltaTimeSeconds": 43200, - "failureFactor": 30, - "roles": { - "realm": [ - { - "id": "73355732-169f-4c0f-8fea-db0794ef5a55", - "name": "default-roles-jimm", - "description": "${role_default-roles}", + "id": "b307f672-e789-4f4d-bb54-051f67686046", + "realm": "jimm", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "73355732-169f-4c0f-8fea-db0794ef5a55", + "name": "default-roles-jimm", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "view-profile", + "manage-account" + ] + } + }, + "clientRole": false, + "containerId": "b307f672-e789-4f4d-bb54-051f67686046", + "attributes": {} + }, + { + "id": "88184f3a-18cc-4d30-af30-a6196075a5aa", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "b307f672-e789-4f4d-bb54-051f67686046", + "attributes": {} + }, + { + "id": "6802acab-ef3f-4d8a-b255-f6eedfcbac25", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "b307f672-e789-4f4d-bb54-051f67686046", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "b776bcfd-140f-4f83-a030-bf12e4060aac", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "062b6466-4c52-4abc-9925-f0dc43c4592e", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "dab02dd5-a48c-4522-b14f-d5c75b421df0", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "6f79a2c0-1c81-47cf-8b2d-a81e04000fb2", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "28335019-75c2-478b-b102-c5b759939c01", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "33e4615a-48ed-4a05-b21f-741a9990410d", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "88df5e21-3fa8-43f6-b0d3-db5f46ff6c7b", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "caabf5fb-5077-403e-a603-a5d562cae25b", + "name": "view-users", + "description": "${role_view-users}", "composite": true, "composites": { - "realm": [ - "offline_access", - "uma_authorization" - ], "client": { - "account": [ - "view-profile", - "manage-account" + "realm-management": [ + "query-groups", + "query-users" ] } }, - "clientRole": false, - "containerId": "b307f672-e789-4f4d-bb54-051f67686046", + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", "attributes": {} }, { - "id": "88184f3a-18cc-4d30-af30-a6196075a5aa", - "name": "offline_access", - "description": "${role_offline-access}", + "id": "4e67550d-4985-45af-b6ea-a05ddd5f5024", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-clients", + "manage-users", + "query-clients", + "query-users", + "view-realm", + "manage-identity-providers", + "query-realms", + "view-users", + "manage-realm", + "view-events", + "manage-events", + "view-identity-providers", + "manage-authorization", + "query-groups", + "impersonation", + "view-authorization", + "view-clients", + "create-client" + ] + } + }, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "85b76caf-c2ea-44a2-b357-3e4d2952fdd5", + "name": "manage-realm", + "description": "${role_manage-realm}", "composite": false, - "clientRole": false, - "containerId": "b307f672-e789-4f4d-bb54-051f67686046", + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", "attributes": {} }, { - "id": "6802acab-ef3f-4d8a-b255-f6eedfcbac25", - "name": "uma_authorization", - "description": "${role_uma_authorization}", + "id": "bb495a95-3958-4d08-a009-09a9dc4c739f", + "name": "view-events", + "description": "${role_view-events}", "composite": false, - "clientRole": false, - "containerId": "b307f672-e789-4f4d-bb54-051f67686046", + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "1c39f2fe-860c-46e0-9501-699efea014ff", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "96bfb00e-b453-4b33-8a6e-29338d05cff1", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "ac4c1b2b-b8b6-4999-9e06-5fd2b18926a7", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "d22d36e3-75b1-4696-8a1e-00cace42ced3", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "12fc3766-0b79-4777-9bde-847697ea91a5", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "f1e8e47a-682b-4103-93ef-6a154fd4a3a8", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "6e7fa140-0b3d-428c-ae09-345b398a1a12", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "attributes": {} + }, + { + "id": "d1efd1a2-e68d-4429-a6d0-92a0b318f70b", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", "attributes": {} } ], - "client": { - "realm-management": [ - { - "id": "b776bcfd-140f-4f83-a030-bf12e4060aac", - "name": "manage-clients", - "description": "${role_manage-clients}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "062b6466-4c52-4abc-9925-f0dc43c4592e", - "name": "manage-users", - "description": "${role_manage-users}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "dab02dd5-a48c-4522-b14f-d5c75b421df0", - "name": "query-clients", - "description": "${role_query-clients}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "6f79a2c0-1c81-47cf-8b2d-a81e04000fb2", - "name": "query-users", - "description": "${role_query-users}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "28335019-75c2-478b-b102-c5b759939c01", - "name": "view-realm", - "description": "${role_view-realm}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "33e4615a-48ed-4a05-b21f-741a9990410d", - "name": "manage-identity-providers", - "description": "${role_manage-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "88df5e21-3fa8-43f6-b0d3-db5f46ff6c7b", - "name": "query-realms", - "description": "${role_query-realms}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "caabf5fb-5077-403e-a603-a5d562cae25b", - "name": "view-users", - "description": "${role_view-users}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "query-groups", - "query-users" - ] - } - }, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "4e67550d-4985-45af-b6ea-a05ddd5f5024", - "name": "realm-admin", - "description": "${role_realm-admin}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "manage-clients", - "manage-users", - "query-clients", - "query-users", - "view-realm", - "manage-identity-providers", - "query-realms", - "view-users", - "manage-realm", - "view-events", - "manage-events", - "view-identity-providers", - "manage-authorization", - "query-groups", - "impersonation", - "view-authorization", - "view-clients", - "create-client" - ] - } - }, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "85b76caf-c2ea-44a2-b357-3e4d2952fdd5", - "name": "manage-realm", - "description": "${role_manage-realm}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "bb495a95-3958-4d08-a009-09a9dc4c739f", - "name": "view-events", - "description": "${role_view-events}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "1c39f2fe-860c-46e0-9501-699efea014ff", - "name": "manage-events", - "description": "${role_manage-events}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "96bfb00e-b453-4b33-8a6e-29338d05cff1", - "name": "view-identity-providers", - "description": "${role_view-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "ac4c1b2b-b8b6-4999-9e06-5fd2b18926a7", - "name": "manage-authorization", - "description": "${role_manage-authorization}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "d22d36e3-75b1-4696-8a1e-00cace42ced3", - "name": "query-groups", - "description": "${role_query-groups}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "12fc3766-0b79-4777-9bde-847697ea91a5", - "name": "impersonation", - "description": "${role_impersonation}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "f1e8e47a-682b-4103-93ef-6a154fd4a3a8", - "name": "view-authorization", - "description": "${role_view-authorization}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "6e7fa140-0b3d-428c-ae09-345b398a1a12", - "name": "view-clients", - "description": "${role_view-clients}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "query-clients" - ] - } - }, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - }, - { - "id": "d1efd1a2-e68d-4429-a6d0-92a0b318f70b", - "name": "create-client", - "description": "${role_create-client}", - "composite": false, - "clientRole": true, - "containerId": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "attributes": {} - } - ], - "security-admin-console": [], - "admin-cli": [], - "account-console": [], - "broker": [ - { - "id": "9150bc28-f740-4e8f-9ce3-13ff80fbd324", - "name": "read-token", - "description": "${role_read-token}", - "composite": false, - "clientRole": true, - "containerId": "a335192a-14e7-4f47-ae24-d9b45a89fb77", - "attributes": {} - } - ], - "account": [ - { - "id": "cfcd7076-3f3e-42fe-9cc2-441122ac5542", - "name": "view-profile", - "description": "${role_view-profile}", - "composite": false, - "clientRole": true, - "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", - "attributes": {} - }, - { - "id": "8ef797db-56e4-43b8-8e44-2ea8fdb5dc51", - "name": "view-groups", - "description": "${role_view-groups}", - "composite": false, - "clientRole": true, - "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", - "attributes": {} - }, - { - "id": "900fb81f-669e-4fef-8e5b-e6702665acba", - "name": "view-consent", - "description": "${role_view-consent}", - "composite": false, - "clientRole": true, - "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", - "attributes": {} - }, - { - "id": "781ba8fe-34e9-41f3-83ce-f2d52c024500", - "name": "manage-account-links", - "description": "${role_manage-account-links}", - "composite": false, - "clientRole": true, - "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", - "attributes": {} - }, - { - "id": "43e59ee5-098a-45d6-8ba8-a7af5d86db2a", - "name": "manage-consent", - "description": "${role_manage-consent}", - "composite": true, - "composites": { - "client": { - "account": [ - "view-consent" - ] - } - }, - "clientRole": true, - "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", - "attributes": {} - }, - { - "id": "b32c7271-04a8-4291-9ab8-d3a4e6879146", - "name": "manage-account", - "description": "${role_manage-account}", - "composite": true, - "composites": { - "client": { - "account": [ - "manage-account-links" - ] - } - }, - "clientRole": true, - "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", - "attributes": {} + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "9150bc28-f740-4e8f-9ce3-13ff80fbd324", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "a335192a-14e7-4f47-ae24-d9b45a89fb77", + "attributes": {} + } + ], + "jimm-device": [], + "account": [ + { + "id": "cfcd7076-3f3e-42fe-9cc2-441122ac5542", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + }, + { + "id": "8ef797db-56e4-43b8-8e44-2ea8fdb5dc51", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + }, + { + "id": "900fb81f-669e-4fef-8e5b-e6702665acba", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + }, + { + "id": "781ba8fe-34e9-41f3-83ce-f2d52c024500", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + }, + { + "id": "43e59ee5-098a-45d6-8ba8-a7af5d86db2a", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } }, - { - "id": "e6352054-fc03-4d1e-acf9-6861f6bd6bd8", - "name": "view-applications", - "description": "${role_view-applications}", - "composite": false, - "clientRole": true, - "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", - "attributes": {} + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + }, + { + "id": "b32c7271-04a8-4291-9ab8-d3a4e6879146", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } }, - { - "id": "ed221e99-697a-4745-9ab7-dc877643f2f6", - "name": "delete-account", - "description": "${role_delete-account}", - "composite": false, - "clientRole": true, - "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", - "attributes": {} - } - ], - "jimm": [] - } - }, - "groups": [], - "defaultRole": { - "id": "73355732-169f-4c0f-8fea-db0794ef5a55", - "name": "default-roles-jimm", - "description": "${role_default-roles}", - "composite": true, - "clientRole": false, - "containerId": "b307f672-e789-4f4d-bb54-051f67686046" - }, - "requiredCredentials": [ - "password" - ], - "otpPolicyType": "totp", - "otpPolicyAlgorithm": "HmacSHA1", - "otpPolicyInitialCounter": 0, - "otpPolicyDigits": 6, - "otpPolicyLookAheadWindow": 1, - "otpPolicyPeriod": 30, - "otpPolicyCodeReusable": false, - "otpSupportedApplications": [ - "totpAppFreeOTPName", - "totpAppGoogleName", - "totpAppMicrosoftAuthenticatorName" - ], - "localizationTexts": {}, - "webAuthnPolicyRpEntityName": "keycloak", - "webAuthnPolicySignatureAlgorithms": [ - "ES256" - ], - "webAuthnPolicyRpId": "", - "webAuthnPolicyAttestationConveyancePreference": "not specified", - "webAuthnPolicyAuthenticatorAttachment": "not specified", - "webAuthnPolicyRequireResidentKey": "not specified", - "webAuthnPolicyUserVerificationRequirement": "not specified", - "webAuthnPolicyCreateTimeout": 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyAcceptableAaguids": [], - "webAuthnPolicyExtraOrigins": [], - "webAuthnPolicyPasswordlessRpEntityName": "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms": [ - "ES256" - ], - "webAuthnPolicyPasswordlessRpId": "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", - "webAuthnPolicyPasswordlessCreateTimeout": 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyPasswordlessAcceptableAaguids": [], - "webAuthnPolicyPasswordlessExtraOrigins": [], - "users": [ - { - "id": "42d4a03f-3a15-4be2-a464-7726984a211d", - "createdTimestamp": 1704705192741, - "username": "service-account-jimm", - "enabled": true, - "totp": false, - "emailVerified": false, - "serviceAccountClientId": "jimm", - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": [ - "default-roles-jimm" - ], - "notBefore": 0, - "groups": [] + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + }, + { + "id": "e6352054-fc03-4d1e-acf9-6861f6bd6bd8", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + }, + { + "id": "ed221e99-697a-4745-9ab7-dc877643f2f6", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "73355732-169f-4c0f-8fea-db0794ef5a55", + "name": "default-roles-jimm", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "b307f672-e789-4f4d-bb54-051f67686046" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "users": [ + { + "id": "8281cec3-5b48-46eb-a41d-72c15ec3f9e0", + "username": "jimm-test", + "email": "jimm-test@canonical.com", + "emailVerified":true, + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "password" + } + ], + "realmRoles": ["user"], + "clientRoles": { + "account": ["view-profile", "manage-account"] } - ], - "scopeMappings": [ + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ { - "clientScope": "offline_access", + "client": "account-console", "roles": [ - "offline_access" + "manage-account", + "view-groups" ] } - ], - "clientScopeMappings": { - "account": [ + ] + }, + "clients": [ + { + "id": "637c1f02-d42d-4fa7-b1af-05b973eac646", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/jimm/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/jimm/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "08730e1b-5e69-45ee-9078-7b28416b24de", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/jimm/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/jimm/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ { - "client": "account-console", - "roles": [ - "manage-account", - "view-groups" - ] + "id": "714b0a7d-e1d3-4f27-b995-bd0aaa3ae2e1", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" ] }, - "clients": [ - { - "id": "637c1f02-d42d-4fa7-b1af-05b973eac646", - "clientId": "account", - "name": "${client_account}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/jimm/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/realms/jimm/account/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] + { + "id": "5c17b099-c66d-4e74-991e-0fd22cb3050e", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" }, - { - "id": "08730e1b-5e69-45ee-9078-7b28416b24de", - "clientId": "account-console", - "name": "${client_account-console}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/jimm/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/realms/jimm/account/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+", - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "714b0a7d-e1d3-4f27-b995-bd0aaa3ae2e1", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - } - ], - "defaultClientScopes": [ - "web-origins", - "acr", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "5c17b099-c66d-4e74-991e-0fd22cb3050e", - "clientId": "admin-cli", - "name": "${client_admin-cli}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": false, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "a335192a-14e7-4f47-ae24-d9b45a89fb77", - "clientId": "broker", - "name": "${client_broker}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "9b8ea7fd-1ea8-43e6-9e2e-71b8f7d878e3", - "clientId": "jimm", - "name": "jimm-testing", - "description": "A client to enable testing JIMM", - "rootUrl": "http://localhost", - "adminUrl": "http://localhost", - "baseUrl": "http://localhost", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": true, - "clientAuthenticatorType": "client-secret", - "secret": "**********", - "redirectUris": [ - "http://localhost/cb" - ], - "webOrigins": [ - "*" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": true, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": true, - "publicClient": false, - "frontchannelLogout": true, - "protocol": "openid-connect", - "attributes": { - "oidc.ciba.grant.enabled": "false", - "client.secret.creation.time": "1704705192", - "backchannel.logout.session.required": "true", - "consent.screen.text": "JIMM consent screen", - "login_theme": "base", - "post.logout.redirect.uris": "localhost:8080/logout", - "oauth2.device.authorization.grant.enabled": "true", - "display.on.consent.screen": "true", - "backchannel.logout.revoke.offline.tokens": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "protocolMappers": [ - { - "id": "28687e0a-ff13-4a16-b8a8-80bd6bbccea9", - "name": "Client Host", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientHost", - "introspection.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientHost", - "jsonType.label": "String" - } - }, - { - "id": "1f6015f3-2812-41a9-896d-155d7d685ca6", - "name": "Client IP Address", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientAddress", - "introspection.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientAddress", - "jsonType.label": "String" - } - }, - { - "id": "5f8f6d15-2f4d-4ce0-908b-9c5fd482e1b5", - "name": "Client ID", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "client_id", - "introspection.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "client_id", - "jsonType.label": "String" - } - } - ], - "defaultClientScopes": [ - "web-origins", - "acr", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", - "clientId": "realm-management", - "name": "${client_realm-management}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "735cae33-7b02-4539-98af-a63bd09c850b", - "clientId": "security-admin-console", - "name": "${client_security-admin-console}", - "rootUrl": "${authAdminUrl}", - "baseUrl": "/admin/jimm/console/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/admin/jimm/console/*" - ], - "webOrigins": [ - "+" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+", - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "f1fdfb97-747e-441f-934d-c1eec0d02a1a", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - } - ], - "defaultClientScopes": [ - "web-origins", - "acr", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - } - ], - "clientScopes": [ - { - "id": "98a43368-13e9-4c5f-8511-95403c22ee64", - "name": "offline_access", - "description": "OpenID Connect built-in scope: offline_access", - "protocol": "openid-connect", - "attributes": { - "consent.screen.text": "${offlineAccessScopeConsentText}", - "display.on.consent.screen": "true" - } + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "a335192a-14e7-4f47-ae24-d9b45a89fb77", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" }, - { - "id": "97752e1e-f7ee-4ba6-91c7-451cfd020425", - "name": "roles", - "description": "OpenID Connect scope for add user roles to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "true", - "consent.screen.text": "${rolesScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "89e366f8-77b7-4428-a946-9c8f427460fd", - "name": "client roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-client-role-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "multivalued": "true", - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "resource_access.${client_id}.roles", - "jsonType.label": "String" - } - }, - { - "id": "aad15d42-ad65-441d-8abd-e8c81b376082", - "name": "realm roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "multivalued": "true", - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "realm_access.roles", - "jsonType.label": "String" - } - }, - { - "id": "8f2d9913-f9ab-4cc2-90b0-7486162eb554", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "access.token.claim": "true" - } - } - ] + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "9b8ea7fd-1ea8-43e6-9e2e-71b8f7d878e3", + "clientId": "jimm-device", + "name": "jimm-testing", + "description": "A client to enable testing JIMM", + "rootUrl": "http://localhost", + "adminUrl": "http://localhost", + "baseUrl": "http://localhost", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": true, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "http://localhost/cb" + ], + "webOrigins": [ + "*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1705363608", + "backchannel.logout.session.required": "true", + "consent.screen.text": "JIMM consent screen", + "login_theme": "base", + "post.logout.redirect.uris": "localhost:8080/logout", + "oauth2.device.authorization.grant.enabled": "true", + "display.on.consent.screen": "true", + "backchannel.logout.revoke.offline.tokens": "false" }, - { - "id": "3924c40c-3941-44cd-a695-eaa7117c8d34", - "name": "profile", - "description": "OpenID Connect built-in scope: profile", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${profileScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "5b20276e-a01d-47fb-95d5-77c8eec04157", - "name": "given name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "firstName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "given_name", - "jsonType.label": "String" - } - }, - { - "id": "92b1456c-e05b-4349-969d-9622c011447d", - "name": "picture", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "picture", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "picture", - "jsonType.label": "String" - } - }, - { - "id": "87b09405-7bc8-4a0f-b7f4-4cb758d47cad", - "name": "zoneinfo", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "zoneinfo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "zoneinfo", - "jsonType.label": "String" - } - }, - { - "id": "94faeffb-5579-4347-9801-a9b095efcc16", - "name": "birthdate", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "birthdate", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "birthdate", - "jsonType.label": "String" - } - }, - { - "id": "2a77708e-f986-4cf8-bce4-ffb29aaab30b", - "name": "gender", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "gender", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "gender", - "jsonType.label": "String" - } - }, - { - "id": "e100936f-87a9-4c74-b38e-30aa82413a48", - "name": "family name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "lastName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "family_name", - "jsonType.label": "String" - } - }, - { - "id": "140dd259-788a-48a1-b890-32c6e6104cf2", - "name": "nickname", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "nickname", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "nickname", - "jsonType.label": "String" - } - }, - { - "id": "08acc240-8601-4235-9e30-c3a2035352e7", - "name": "website", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "website", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "website", - "jsonType.label": "String" - } - }, - { - "id": "4d4806cb-3c58-49ef-8065-e327af8dc748", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - }, - { - "id": "c958e838-0bb7-4aeb-b387-91147ca7e627", - "name": "full name", - "protocol": "openid-connect", - "protocolMapper": "oidc-full-name-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "introspection.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - }, - { - "id": "9019e06d-3949-4420-8025-5f977eeb94f7", - "name": "username", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "preferred_username", - "jsonType.label": "String" - } - }, - { - "id": "b62b12bb-cdf6-4a96-a3d9-99dd6979ab36", - "name": "middle name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "middleName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "middle_name", - "jsonType.label": "String" - } - }, - { - "id": "045ffc8b-7898-4276-9feb-adf36c3b36c2", - "name": "profile", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "profile", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "profile", - "jsonType.label": "String" - } - }, - { - "id": "5e9d597f-8064-4b74-9853-93105fa8b643", - "name": "updated at", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "updatedAt", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "updated_at", - "jsonType.label": "long" - } + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "28687e0a-ff13-4a16-b8a8-80bd6bbccea9", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" } - ] - }, - { - "id": "d4241c04-74cb-4463-bc4d-0c038f183086", - "name": "phone", - "description": "OpenID Connect built-in scope: phone", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${phoneScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "b58ff5e3-bc9a-464a-8877-119e491c8213", - "name": "phone number verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "phoneNumberVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number_verified", - "jsonType.label": "boolean" - } - }, - { - "id": "8e66dc5f-59ff-4e9d-bebf-45f43a350d4e", - "name": "phone number", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "phoneNumber", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number", - "jsonType.label": "String" - } + }, + { + "id": "1f6015f3-2812-41a9-896d-155d7d685ca6", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" } - ] - }, - { - "id": "54eae197-3ef1-4c49-837d-23c8434b2b97", - "name": "address", - "description": "OpenID Connect built-in scope: address", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${addressScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "0bbada0c-5f98-4f75-8820-cb4ac3383bb6", - "name": "address", - "protocol": "openid-connect", - "protocolMapper": "oidc-address-mapper", - "consentRequired": false, - "config": { - "user.attribute.formatted": "formatted", - "user.attribute.country": "country", - "introspection.token.claim": "true", - "user.attribute.postal_code": "postal_code", - "userinfo.token.claim": "true", - "user.attribute.street": "street", - "id.token.claim": "true", - "user.attribute.region": "region", - "access.token.claim": "true", - "user.attribute.locality": "locality" - } + }, + { + "id": "5f8f6d15-2f4d-4ce0-908b-9c5fd482e1b5", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "client_id", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "client_id", + "jsonType.label": "String" } - ] + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" }, - { - "id": "04b49931-0780-4f62-b098-6de969fc5ce2", - "name": "web-origins", - "description": "OpenID Connect scope for add allowed web origins to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false", - "consent.screen.text": "" - }, - "protocolMappers": [ - { - "id": "30260d43-3298-418a-9f5c-28e050a7e429", - "name": "allowed web origins", - "protocol": "openid-connect", - "protocolMapper": "oidc-allowed-origins-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "access.token.claim": "true" - } - } - ] + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "735cae33-7b02-4539-98af-a63bd09c850b", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/jimm/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/jimm/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" }, - { - "id": "9fd9c2e6-6b26-4026-bb1b-1003c2e1934d", - "name": "email", - "description": "OpenID Connect built-in scope: email", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${emailScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "6b1105ca-f3c5-447b-ba38-66393e8b4ce7", - "name": "email", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "email", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email", - "jsonType.label": "String" - } - }, - { - "id": "9ccb464e-c78d-4c8f-8a24-4b4754d22537", - "name": "email verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "emailVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email_verified", - "jsonType.label": "boolean" - } + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "f1fdfb97-747e-441f-934d-c1eec0d02a1a", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" } - ] + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "98a43368-13e9-4c5f-8511-95403c22ee64", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "97752e1e-f7ee-4ba6-91c7-451cfd020425", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" }, - { - "id": "019a385b-2631-41fb-a444-2d636105c506", - "name": "microprofile-jwt", - "description": "Microprofile - JWT built-in scope", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "a1ecc841-161d-4de6-a2a2-2b4fe964ff50", - "name": "upn", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "upn", - "jsonType.label": "String" - } - }, - { - "id": "8d80ce4b-3a01-48f9-974f-6a5e2074ccc6", - "name": "groups", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "multivalued": "true", - "userinfo.token.claim": "true", - "user.attribute": "foo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "groups", - "jsonType.label": "String" - } + "protocolMappers": [ + { + "id": "89e366f8-77b7-4428-a946-9c8f427460fd", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String" } - ] - }, - { - "id": "6e520aff-0c54-4f06-af15-8e77874d5145", - "name": "role_list", - "description": "SAML role list", - "protocol": "saml", - "attributes": { - "consent.screen.text": "${samlRoleListScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "8153fa75-5dfa-4caa-a52c-f39d61ee4a28", - "name": "role list", - "protocol": "saml", - "protocolMapper": "saml-role-list-mapper", - "consentRequired": false, - "config": { - "single": "false", - "attribute.nameformat": "Basic", - "attribute.name": "Role" - } + }, + { + "id": "aad15d42-ad65-441d-8abd-e8c81b376082", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String" } - ] - }, - { - "id": "776d1bfd-6192-48a4-a9c8-67b33752bf28", - "name": "acr", - "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "440f0736-dac9-4a4d-918d-0255a674fb46", - "name": "acr loa level", - "protocol": "openid-connect", - "protocolMapper": "oidc-acr-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "introspection.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } + }, + { + "id": "8f2d9913-f9ab-4cc2-90b0-7486162eb554", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" } - ] - } - ], - "defaultDefaultClientScopes": [ - "role_list", - "profile", - "email", - "roles", - "web-origins", - "acr" - ], - "defaultOptionalClientScopes": [ - "offline_access", - "address", - "phone", - "microprofile-jwt" - ], - "browserSecurityHeaders": { - "contentSecurityPolicyReportOnly": "", - "xContentTypeOptions": "nosniff", - "referrerPolicy": "no-referrer", - "xRobotsTag": "none", - "xFrameOptions": "SAMEORIGIN", - "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "xXSSProtection": "1; mode=block", - "strictTransportSecurity": "max-age=31536000; includeSubDomains" + } + ] }, - "smtpServer": {}, - "eventsEnabled": false, - "eventsListeners": [ - "jboss-logging" - ], - "enabledEventTypes": [], - "adminEventsEnabled": false, - "adminEventsDetailsEnabled": false, - "identityProviders": [], - "identityProviderMappers": [], - "components": { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ - { - "id": "9f8786d7-0dbd-4c67-81cf-97d8870ae747", - "name": "Max Clients Limit", - "providerId": "max-clients", - "subType": "anonymous", - "subComponents": {}, + { + "id": "3924c40c-3941-44cd-a695-eaa7117c8d34", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "5b20276e-a01d-47fb-95d5-77c8eec04157", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, "config": { - "max-clients": [ - "200" - ] + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" } }, { - "id": "337e0eb8-71d0-472f-a402-6e8656fb2158", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "anonymous", - "subComponents": {}, + "id": "92b1456c-e05b-4349-969d-9622c011447d", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, "config": { - "allowed-protocol-mapper-types": [ - "oidc-sha256-pairwise-sub-mapper", - "oidc-full-name-mapper", - "saml-user-attribute-mapper", - "saml-user-property-mapper", - "oidc-address-mapper", - "saml-role-list-mapper", - "oidc-usermodel-attribute-mapper", - "oidc-usermodel-property-mapper" - ] + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" } }, { - "id": "9aed0a59-b7e6-41f8-a862-83401d1f6a26", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "anonymous", - "subComponents": {}, + "id": "87b09405-7bc8-4a0f-b7f4-4cb758d47cad", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, "config": { - "allow-default-scopes": [ - "true" - ] + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" } }, { - "id": "d1af4a25-c22a-4e0a-877d-671a13598e44", - "name": "Trusted Hosts", - "providerId": "trusted-hosts", - "subType": "anonymous", - "subComponents": {}, + "id": "94faeffb-5579-4347-9801-a9b095efcc16", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, "config": { - "host-sending-registration-request-must-match": [ - "true" - ], - "client-uris-must-match": [ - "true" - ] + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" } }, { - "id": "f9077f05-c3a6-4dcc-8db1-79b6e7f69366", - "name": "Full Scope Disabled", - "providerId": "scope", - "subType": "anonymous", - "subComponents": {}, - "config": {} + "id": "2a77708e-f986-4cf8-bce4-ffb29aaab30b", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } }, { - "id": "0a184f0f-92bb-42c6-a0d4-a4cdabc38d76", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "authenticated", - "subComponents": {}, + "id": "e100936f-87a9-4c74-b38e-30aa82413a48", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, "config": { - "allowed-protocol-mapper-types": [ - "oidc-usermodel-property-mapper", - "oidc-sha256-pairwise-sub-mapper", - "oidc-full-name-mapper", - "oidc-address-mapper", - "saml-user-attribute-mapper", - "oidc-usermodel-attribute-mapper", - "saml-role-list-mapper", - "saml-user-property-mapper" - ] + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" } }, { - "id": "cf4d9180-6c03-4c81-897b-2654e1a9157a", - "name": "Consent Required", - "providerId": "consent-required", - "subType": "anonymous", - "subComponents": {}, - "config": {} + "id": "140dd259-788a-48a1-b890-32c6e6104cf2", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } }, { - "id": "8d7f40d4-70d5-4d58-919d-a35fc1cb589d", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "authenticated", - "subComponents": {}, + "id": "08acc240-8601-4235-9e30-c3a2035352e7", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, "config": { - "allow-default-scopes": [ - "true" - ] + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" } - } - ], - "org.keycloak.keys.KeyProvider": [ + }, { - "id": "0c361a8c-35ab-4ba7-91b3-98ba3d388db4", - "name": "rsa-enc-generated", - "providerId": "rsa-enc-generated", - "subComponents": {}, + "id": "4d4806cb-3c58-49ef-8065-e327af8dc748", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, "config": { - "priority": [ - "100" - ], - "algorithm": [ - "RSA-OAEP" - ] + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" } }, { - "id": "250164bf-1952-4139-b247-910f71a852ba", - "name": "aes-generated", - "providerId": "aes-generated", - "subComponents": {}, + "id": "c958e838-0bb7-4aeb-b387-91147ca7e627", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, "config": { - "priority": [ - "100" - ] + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" } }, { - "id": "d74a3cae-1559-41ca-82e4-635192d76d41", - "name": "hmac-generated", - "providerId": "hmac-generated", - "subComponents": {}, + "id": "9019e06d-3949-4420-8025-5f977eeb94f7", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, "config": { - "priority": [ - "100" - ], - "algorithm": [ - "HS256" - ] + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" } }, { - "id": "205539ad-c37e-44ce-adea-4d598441e9cd", - "name": "rsa-generated", - "providerId": "rsa-generated", - "subComponents": {}, + "id": "b62b12bb-cdf6-4a96-a3d9-99dd6979ab36", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, "config": { - "priority": [ - "100" - ] - } - } - ] - }, - "internationalizationEnabled": false, - "supportedLocales": [], - "authenticationFlows": [ - { - "id": "9b835aa1-820a-4e71-b41b-29397ff5d805", - "alias": "Account verification options", - "description": "Method with which to verity the existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-email-verification", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Verify Existing Account by Re-authentication", - "userSetupAllowed": false - } - ] - }, - { - "id": "0eb823f9-df61-49b1-bde8-4749eb36a677", - "alias": "Browser - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" } - ] - }, - { - "id": "bf6d7a73-2723-4314-83c6-a9f8f137cd49", - "alias": "Direct Grant - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "9d86c8e0-c5ad-401a-874e-fe41a2ac0362", - "alias": "First broker login - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "2b399312-003c-4b09-af85-016bdb78f018", - "alias": "Handle Existing Account", - "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-confirm-link", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Account verification options", - "userSetupAllowed": false - } - ] - }, - { - "id": "8c62d4bb-dd39-44fa-834d-978e6c0efd86", - "alias": "Reset - Conditional OTP", - "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "523dbb97-675d-4a82-b8d0-8f55f953cc5d", - "alias": "User creation or linking", - "description": "Flow for the existing/non-existing user alternatives", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "create unique user config", - "authenticator": "idp-create-user-if-unique", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Handle Existing Account", - "userSetupAllowed": false + }, + { + "id": "045ffc8b-7898-4276-9feb-adf36c3b36c2", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" } - ] - }, - { - "id": "7484294b-3b44-4603-9ff3-3cc1d9f898b1", - "alias": "Verify Existing Account by Re-authentication", - "description": "Reauthentication of existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "First broker login - Conditional OTP", - "userSetupAllowed": false + }, + { + "id": "5e9d597f-8064-4b74-9853-93105fa8b643", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" } - ] + } + ] + }, + { + "id": "d4241c04-74cb-4463-bc4d-0c038f183086", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" }, - { - "id": "fb087480-2aaf-421f-a3e3-bab2f40f43cb", - "alias": "browser", - "description": "browser based authentication", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-cookie", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "identity-provider-redirector", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 25, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "forms", - "userSetupAllowed": false + "protocolMappers": [ + { + "id": "b58ff5e3-bc9a-464a-8877-119e491c8213", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" } - ] - }, - { - "id": "25f1d47f-fe02-4b05-8100-56229d8930ac", - "alias": "clients", - "description": "Base authentication for clients", - "providerId": "client-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "client-secret", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-secret-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-x509", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 40, - "autheticatorFlow": false, - "userSetupAllowed": false + }, + { + "id": "8e66dc5f-59ff-4e9d-bebf-45f43a350d4e", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" } - ] + } + ] + }, + { + "id": "54eae197-3ef1-4c49-837d-23c8434b2b97", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" }, - { - "id": "1c9030e5-5be1-44dd-bc9b-326e5fc8addc", - "alias": "direct grant", - "description": "OpenID Connect Resource Owner Grant", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "direct-grant-validate-username", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "Direct Grant - Conditional OTP", - "userSetupAllowed": false + "protocolMappers": [ + { + "id": "0bbada0c-5f98-4f75-8820-cb4ac3383bb6", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" } - ] + } + ] + }, + { + "id": "04b49931-0780-4f62-b098-6de969fc5ce2", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" }, - { - "id": "dc2a8b23-b362-4f25-bf40-44259d5d8c11", - "alias": "docker auth", - "description": "Used by Docker clients to authenticate against the IDP", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "docker-http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false + "protocolMappers": [ + { + "id": "30260d43-3298-418a-9f5c-28e050a7e429", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" } - ] + } + ] + }, + { + "id": "9fd9c2e6-6b26-4026-bb1b-1003c2e1934d", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" }, - { - "id": "0d7b1bf4-440e-4c32-a2b2-51476ff732bd", - "alias": "first broker login", - "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "review profile config", - "authenticator": "idp-review-profile", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "User creation or linking", - "userSetupAllowed": false + "protocolMappers": [ + { + "id": "6b1105ca-f3c5-447b-ba38-66393e8b4ce7", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" } - ] - }, - { - "id": "b7bf2c5c-d6d7-4ba0-a996-d786f18ba198", - "alias": "forms", - "description": "Username, password, otp and other auth forms.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Browser - Conditional OTP", - "userSetupAllowed": false + }, + { + "id": "9ccb464e-c78d-4c8f-8a24-4b4754d22537", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" } - ] + } + ] + }, + { + "id": "019a385b-2631-41fb-a444-2d636105c506", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" }, - { - "id": "9342460d-847e-44fe-b248-bd8bfe62af2e", - "alias": "registration", - "description": "registration flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-page-form", - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": true, - "flowAlias": "registration form", - "userSetupAllowed": false + "protocolMappers": [ + { + "id": "a1ecc841-161d-4de6-a2a2-2b4fe964ff50", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" } - ] - }, - { - "id": "3a29625a-d3f2-4b99-a2e3-e7f3d44f1a69", - "alias": "registration form", - "description": "registration form", - "providerId": "form-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-user-creation", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-password-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 50, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-recaptcha-action", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 60, - "autheticatorFlow": false, - "userSetupAllowed": false + }, + { + "id": "8d80ce4b-3a01-48f9-974f-6a5e2074ccc6", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "userinfo.token.claim": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" } - ] + } + ] + }, + { + "id": "6e520aff-0c54-4f06-af15-8e77874d5145", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" }, - { - "id": "8577b54d-6720-4cf0-82b1-29b22761862a", - "alias": "reset credentials", - "description": "Reset credentials for a user if they forgot their password or something", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "reset-credentials-choose-user", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-credential-email", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 40, - "autheticatorFlow": true, - "flowAlias": "Reset - Conditional OTP", - "userSetupAllowed": false + "protocolMappers": [ + { + "id": "8153fa75-5dfa-4caa-a52c-f39d61ee4a28", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" } - ] + } + ] + }, + { + "id": "776d1bfd-6192-48a4-a9c8-67b33752bf28", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" }, - { - "id": "2c171aa0-b957-469b-a98a-a2f69dfc48f6", - "alias": "saml ecp", - "description": "SAML ECP Profile Authentication Flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false + "protocolMappers": [ + { + "id": "440f0736-dac9-4a4d-918d-0255a674fb46", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" } - ] - } - ], - "authenticatorConfig": [ + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ { - "id": "e6a925a0-fa76-4ea8-accc-548a2e7f97f8", - "alias": "create unique user config", + "id": "9f8786d7-0dbd-4c67-81cf-97d8870ae747", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, "config": { - "require.password.update.after.registration": "false" + "max-clients": [ + "200" + ] } }, { - "id": "c980449f-5ef2-447a-b78f-00a0e39bcfd6", - "alias": "review profile config", + "id": "337e0eb8-71d0-472f-a402-6e8656fb2158", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, "config": { - "update.profile.on.first.login": "missing" + "allowed-protocol-mapper-types": [ + "oidc-full-name-mapper", + "saml-user-property-mapper", + "saml-role-list-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-attribute-mapper", + "oidc-address-mapper", + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper" + ] } - } - ], - "requiredActions": [ + }, { - "alias": "CONFIGURE_TOTP", - "name": "Configure OTP", - "providerId": "CONFIGURE_TOTP", - "enabled": true, - "defaultAction": false, - "priority": 10, - "config": {} + "id": "9aed0a59-b7e6-41f8-a862-83401d1f6a26", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } }, { - "alias": "TERMS_AND_CONDITIONS", - "name": "Terms and Conditions", - "providerId": "TERMS_AND_CONDITIONS", - "enabled": false, - "defaultAction": false, - "priority": 20, - "config": {} + "id": "d1af4a25-c22a-4e0a-877d-671a13598e44", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } }, { - "alias": "UPDATE_PASSWORD", - "name": "Update Password", - "providerId": "UPDATE_PASSWORD", - "enabled": true, - "defaultAction": false, - "priority": 30, + "id": "f9077f05-c3a6-4dcc-8db1-79b6e7f69366", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, "config": {} }, { - "alias": "UPDATE_PROFILE", - "name": "Update Profile", - "providerId": "UPDATE_PROFILE", - "enabled": true, - "defaultAction": false, - "priority": 40, - "config": {} + "id": "0a184f0f-92bb-42c6-a0d4-a4cdabc38d76", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-role-list-mapper", + "oidc-full-name-mapper", + "saml-user-attribute-mapper", + "oidc-address-mapper", + "saml-user-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-usermodel-property-mapper" + ] + } }, { - "alias": "VERIFY_EMAIL", - "name": "Verify Email", - "providerId": "VERIFY_EMAIL", - "enabled": true, - "defaultAction": false, - "priority": 50, + "id": "cf4d9180-6c03-4c81-897b-2654e1a9157a", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, "config": {} }, { - "alias": "delete_account", - "name": "Delete Account", - "providerId": "delete_account", - "enabled": false, - "defaultAction": false, - "priority": 60, - "config": {} + "id": "8d7f40d4-70d5-4d58-919d-a35fc1cb589d", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "0c361a8c-35ab-4ba7-91b3-98ba3d388db4", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } }, { - "alias": "webauthn-register", - "name": "Webauthn Register", - "providerId": "webauthn-register", - "enabled": true, - "defaultAction": false, - "priority": 70, - "config": {} + "id": "250164bf-1952-4139-b247-910f71a852ba", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } }, { - "alias": "webauthn-register-passwordless", - "name": "Webauthn Register Passwordless", - "providerId": "webauthn-register-passwordless", - "enabled": true, - "defaultAction": false, - "priority": 80, - "config": {} + "id": "d74a3cae-1559-41ca-82e4-635192d76d41", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } }, { - "alias": "update_user_locale", - "name": "Update User Locale", - "providerId": "update_user_locale", - "enabled": true, - "defaultAction": false, - "priority": 1000, - "config": {} + "id": "205539ad-c37e-44ce-adea-4d598441e9cd", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } } - ], - "browserFlow": "browser", - "registrationFlow": "registration", - "directGrantFlow": "direct grant", - "resetCredentialsFlow": "reset credentials", - "clientAuthenticationFlow": "clients", - "dockerAuthenticationFlow": "docker auth", - "attributes": { - "cibaBackchannelTokenDeliveryMode": "poll", - "cibaExpiresIn": "120", - "cibaAuthRequestedUserHint": "login_hint", - "oauth2DeviceCodeLifespan": "600", - "clientOfflineSessionMaxLifespan": "0", - "oauth2DevicePollingInterval": "5", - "clientSessionIdleTimeout": "0", - "parRequestUriLifespan": "60", - "clientSessionMaxLifespan": "0", - "clientOfflineSessionIdleTimeout": "0", - "cibaInterval": "5", - "realmReusableOtpCode": "false" + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "9b835aa1-820a-4e71-b41b-29397ff5d805", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "0eb823f9-df61-49b1-bde8-4749eb36a677", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "bf6d7a73-2723-4314-83c6-a9f8f137cd49", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "9d86c8e0-c5ad-401a-874e-fe41a2ac0362", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "2b399312-003c-4b09-af85-016bdb78f018", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "8c62d4bb-dd39-44fa-834d-978e6c0efd86", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "523dbb97-675d-4a82-b8d0-8f55f953cc5d", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "7484294b-3b44-4603-9ff3-3cc1d9f898b1", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "fb087480-2aaf-421f-a3e3-bab2f40f43cb", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "25f1d47f-fe02-4b05-8100-56229d8930ac", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "1c9030e5-5be1-44dd-bc9b-326e5fc8addc", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "dc2a8b23-b362-4f25-bf40-44259d5d8c11", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "0d7b1bf4-440e-4c32-a2b2-51476ff732bd", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "b7bf2c5c-d6d7-4ba0-a996-d786f18ba198", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "9342460d-847e-44fe-b248-bd8bfe62af2e", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "3a29625a-d3f2-4b99-a2e3-e7f3d44f1a69", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "8577b54d-6720-4cf0-82b1-29b22761862a", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "2c171aa0-b957-469b-a98a-a2f69dfc48f6", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "e6a925a0-fa76-4ea8-accc-548a2e7f97f8", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "c980449f-5ef2-447a-b78f-00a0e39bcfd6", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} }, - "keycloakVersion": "23.0.3", - "userManagedAccessAllowed": false, - "clientProfiles": { - "profiles": [] + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} }, - "clientPolicies": { - "policies": [] + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} } - } \ No newline at end of file + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "clientOfflineSessionMaxLifespan": "0", + "oauth2DevicePollingInterval": "5", + "clientSessionIdleTimeout": "0", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5", + "realmReusableOtpCode": "false" + }, + "keycloakVersion": "23.0.3", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} \ No newline at end of file From a72e4865fd76d3175b3c1fed1cae52fd7dfa9995 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 19 Jan 2024 08:44:09 -0800 Subject: [PATCH 042/126] CSS-6765 list svcacc cloud creds (#1136) * Add ListServiceAccountCredentials * Update user.go * PR comments --- api/params/params.go | 8 ++ internal/jujuapi/cloud.go | 14 +- internal/jujuapi/export_test.go | 4 + internal/jujuapi/service_account.go | 57 ++++---- internal/jujuapi/service_account_test.go | 159 ++++++++++++++++++++--- internal/openfga/user.go | 10 ++ 6 files changed, 206 insertions(+), 46 deletions(-) diff --git a/api/params/params.go b/api/params/params.go index 9e9af0eb2..a929cc717 100644 --- a/api/params/params.go +++ b/api/params/params.go @@ -409,3 +409,11 @@ type UpdateServiceAccountCredentialsRequest struct { // ClientID holds the client id of the service account. ClientID string `json:"client-id"` } + +// ListServiceAccountCredentialsRequest holds a request to list +// a service accounts cloud credentials. +type ListServiceAccountCredentialsRequest struct { + jujuparams.CloudCredentialArgs + // ClientID holds the client id of the service account. + ClientID string `json:"client-id"` +} diff --git a/internal/jujuapi/cloud.go b/internal/jujuapi/cloud.go index 40a00841d..ba3db7a90 100644 --- a/internal/jujuapi/cloud.go +++ b/internal/jujuapi/cloud.go @@ -304,6 +304,10 @@ func userModelAccess(ctx context.Context, user *openfga.User, model names.ModelT // CredentialContents implements the CredentialContents method of the Cloud (v5) facade. func (r *controllerRoot) CredentialContents(ctx context.Context, args jujuparams.CloudCredentialArgs) (jujuparams.CredentialContentResults, error) { + return getIdentityCredentials(ctx, r.user, r.jimm, args) +} + +func getIdentityCredentials(ctx context.Context, user *openfga.User, j JIMM, args jujuparams.CloudCredentialArgs) (jujuparams.CredentialContentResults, error) { const op = errors.Op("jujuapi.CredentialContents") credentialContents := func(c *dbmodel.CloudCredential) (*jujuparams.ControllerCredentialInfo, error) { @@ -316,13 +320,13 @@ func (r *controllerRoot) CredentialContents(ctx context.Context, args jujuparams content.Valid = &c.Valid.Bool } var err error - content.Attributes, _, err = r.jimm.GetCloudCredentialAttributes(ctx, r.user, c, args.IncludeSecrets) + content.Attributes, _, err = j.GetCloudCredentialAttributes(ctx, user, c, args.IncludeSecrets) if err != nil { return nil, errors.E(err) } mas := make([]jujuparams.ModelAccess, len(c.Models)) for i, m := range c.Models { - userModelAccess, err := userModelAccess(ctx, r.user, m.ResourceTag()) + userModelAccess, err := userModelAccess(ctx, user, m.ResourceTag()) if err != nil { return nil, errors.E(err) } @@ -339,8 +343,8 @@ func (r *controllerRoot) CredentialContents(ctx context.Context, args jujuparams results := make([]jujuparams.CredentialContentResult, len(args.Credentials)) for i, arg := range args.Credentials { - cct := names.NewCloudCredentialTag(fmt.Sprintf("%s/%s/%s", arg.CloudName, r.user.Name, arg.CredentialName)) - cred, err := r.jimm.GetCloudCredential(ctx, r.user, cct) + cct := names.NewCloudCredentialTag(fmt.Sprintf("%s/%s/%s", arg.CloudName, user.Name, arg.CredentialName)) + cred, err := j.GetCloudCredential(ctx, user, cct) if err != nil { results[i].Error = mapError(errors.E(op, err)) continue @@ -354,7 +358,7 @@ func (r *controllerRoot) CredentialContents(ctx context.Context, args jujuparams return jujuparams.CredentialContentResults{Results: results}, nil } - err := r.jimm.ForEachUserCloudCredential(ctx, r.user.Identity, names.CloudTag{}, func(c *dbmodel.CloudCredential) error { + err := j.ForEachUserCloudCredential(ctx, user.Identity, names.CloudTag{}, func(c *dbmodel.CloudCredential) error { var result jujuparams.CredentialContentResult var err error result.Result, err = credentialContents(c) diff --git a/internal/jujuapi/export_test.go b/internal/jujuapi/export_test.go index dcc4c2d43..e48b6995b 100644 --- a/internal/jujuapi/export_test.go +++ b/internal/jujuapi/export_test.go @@ -53,6 +53,10 @@ func NewControllerRoot(j JIMM, p Params) *controllerRoot { return newControllerRoot(j, p) } +func (r *controllerRoot) GetServiceAccount(ctx context.Context, clientID string) (*openfga.User, error) { + return r.getServiceAccount(ctx, clientID) +} + var SetUser = func(r *controllerRoot, u *openfga.User) { r.mu.Lock() r.user = u diff --git a/internal/jujuapi/service_account.go b/internal/jujuapi/service_account.go index e9ecc1b6c..c036aa9bd 100644 --- a/internal/jujuapi/service_account.go +++ b/internal/jujuapi/service_account.go @@ -13,7 +13,6 @@ import ( "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/openfga" - ofganames "github.com/canonical/jimm/internal/openfga/names" jimmnames "github.com/canonical/jimm/pkg/names" ) @@ -30,6 +29,29 @@ func (r *controllerRoot) AddServiceAccount(ctx context.Context, req apiparams.Ad return r.jimm.AddServiceAccount(ctx, r.user, req.ClientID) } +// getServiceAccount validates the incoming identity has administrator permission +// on the service account and returns the service account identity. +func (r *controllerRoot) getServiceAccount(ctx context.Context, clientID string) (*openfga.User, error) { + if !jimmnames.IsValidServiceAccountId(clientID) { + return nil, errors.E(errors.CodeBadRequest, "invalid client ID") + } + + ok, err := r.user.IsServiceAccountAdmin(ctx, jimmnames.NewServiceAccountTag(clientID)) + if err != nil { + return nil, errors.E(err) + } + if !ok { + return nil, errors.E(errors.CodeUnauthorized, "unauthorized") + } + + var targetIdentityModel dbmodel.Identity + targetIdentityModel.SetTag(names.NewUserTag(clientID)) + if err := r.jimm.DB().GetIdentity(ctx, &targetIdentityModel); err != nil { + return nil, errors.E(err) + } + return openfga.NewUser(&targetIdentityModel, r.jimm.AuthorizationClient()), nil +} + // UpdateServiceAccountCredentialsCheckModels updates a set of cloud credentials' content. // If there are any models that are using a credential and these models // are not going to be visible with updated credential content, @@ -39,29 +61,10 @@ func (r *controllerRoot) AddServiceAccount(ctx context.Context, req apiparams.Ad func (r *controllerRoot) UpdateServiceAccountCredentials(ctx context.Context, req apiparams.UpdateServiceAccountCredentialsRequest) (jujuparams.UpdateCredentialResults, error) { const op = errors.Op("jujuapi.UpdateServiceAccountCredentials") - if !jimmnames.IsValidServiceAccountId(req.ClientID) { - return jujuparams.UpdateCredentialResults{}, errors.E(op, errors.CodeBadRequest, "invalid client ID") - } - - tuple := openfga.Tuple{ - Object: ofganames.ConvertTag(r.user.ResourceTag()), - Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(req.ClientID)), - } - ok, err := r.jimm.AuthorizationClient().CheckRelation(ctx, tuple, false) + targetIdentity, err := r.getServiceAccount(ctx, req.ClientID) if err != nil { - return jujuparams.UpdateCredentialResults{}, errors.E(op, errors.CodeOpenFGARequestFailed, "unable to determine permissions") - } - if !ok { - return jujuparams.UpdateCredentialResults{}, errors.E(op, errors.CodeUnauthorized, "unauthorized") - } - - var targetUserModel dbmodel.Identity - targetUserModel.SetTag(names.NewUserTag(req.ClientID)) - if err := r.jimm.DB().GetIdentity(ctx, &targetUserModel); err != nil { return jujuparams.UpdateCredentialResults{}, errors.E(op, err) } - targetUser := openfga.NewUser(&targetUserModel, r.jimm.AuthorizationClient()) results := jujuparams.UpdateCredentialResults{ Results: make([]jujuparams.UpdateCredentialResult, len(req.Credentials)), @@ -72,7 +75,7 @@ func (r *controllerRoot) UpdateServiceAccountCredentials(ctx context.Context, re var tag names.CloudCredentialTag tag, err = names.ParseCloudCredentialTag(credential.Tag) if err == nil { - res, err = r.jimm.UpdateCloudCredential(ctx, targetUser, jimm.UpdateCloudCredentialArgs{ + res, err = r.jimm.UpdateCloudCredential(ctx, targetIdentity, jimm.UpdateCloudCredentialArgs{ CredentialTag: tag, Credential: credential.Credential, // Check that all credentials are valid. @@ -90,3 +93,13 @@ func (r *controllerRoot) UpdateServiceAccountCredentials(ctx context.Context, re } return results, nil } + +func (r *controllerRoot) ListServiceAccountCredentials(ctx context.Context, req apiparams.ListServiceAccountCredentialsRequest) (jujuparams.CredentialContentResults, error) { + const op = errors.Op("jujuapi.UpdateServiceAccountCredentials") + + targetIdentity, err := r.getServiceAccount(ctx, req.ClientID) + if err != nil { + return jujuparams.CredentialContentResults{}, errors.E(op, err) + } + return getIdentityCredentials(ctx, targetIdentity, r.jimm, req.CloudCredentialArgs) +} diff --git a/internal/jujuapi/service_account_test.go b/internal/jujuapi/service_account_test.go index e9bd1b339..8e7f77790 100644 --- a/internal/jujuapi/service_account_test.go +++ b/internal/jujuapi/service_account_test.go @@ -69,6 +69,71 @@ func TestAddServiceAccount(t *testing.T) { } } +func TestGetServiceAccount(t *testing.T) { + c := qt.New(t) + + tests := []struct { + about string + clientID string + addTuples []openfga.Tuple + username string + expectedError string + }{{ + about: "Valid request", + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + username: "alice", + addTuples: []openfga.Tuple{{ + Object: ofganames.ConvertTag(names.NewUserTag("alice")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), + }}, + }, { + about: "Missing service account administrator permission", + username: "alice", + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + expectedError: "unauthorized", + }, { + about: "Invalid Client ID", + username: "alice", + clientID: "_123_", + expectedError: "invalid client ID", + }} + + for _, test := range tests { + test := test + c.Run(test.about, func(c *qt.C) { + ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) + c.Assert(err, qt.IsNil) + pgDb := db.Database{ + DB: jimmtest.PostgresDB(c, nil), + } + err = pgDb.Migrate(context.Background(), false) + c.Assert(err, qt.IsNil) + jimm := &jimmtest.JIMM{ + AuthorizationClient_: func() *openfga.OFGAClient { return ofgaClient }, + DB_: func() *db.Database { return &pgDb }, + } + var u dbmodel.Identity + u.SetTag(names.NewUserTag(test.username)) + user := openfga.NewUser(&u, ofgaClient) + cr := jujuapi.NewControllerRoot(jimm, jujuapi.Params{}) + jujuapi.SetUser(cr, user) + + if len(test.addTuples) > 0 { + ofgaClient.AddRelation(context.Background(), test.addTuples...) + } + + res, err := cr.GetServiceAccount(context.Background(), test.clientID) + if test.expectedError == "" { + c.Assert(err, qt.IsNil) + c.Assert(res.Identity.Name, qt.Equals, test.clientID) + } else { + c.Assert(err, qt.ErrorMatches, test.expectedError) + } + }) + } +} + func TestUpdateServiceAccountCredentials(t *testing.T) { c := qt.New(t) @@ -149,26 +214,80 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), }}, - }, { - about: "Missing service account administrator permission", - updateCloudCredential: func(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) { - return nil, nil + }} + + for _, test := range tests { + test := test + c.Run(test.about, func(c *qt.C) { + ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) + c.Assert(err, qt.IsNil) + pgDb := db.Database{ + DB: jimmtest.PostgresDB(c, nil), + } + err = pgDb.Migrate(context.Background(), false) + c.Assert(err, qt.IsNil) + jimm := &jimmtest.JIMM{ + AuthorizationClient_: func() *openfga.OFGAClient { return ofgaClient }, + UpdateCloudCredential_: test.updateCloudCredential, + DB_: func() *db.Database { return &pgDb }, + } + var u dbmodel.Identity + u.SetTag(names.NewUserTag(test.username)) + user := openfga.NewUser(&u, ofgaClient) + cr := jujuapi.NewControllerRoot(jimm, jujuapi.Params{}) + jujuapi.SetUser(cr, user) + + if len(test.addTuples) > 0 { + ofgaClient.AddRelation(context.Background(), test.addTuples...) + } + + res, err := cr.UpdateServiceAccountCredentials(context.Background(), test.args) + if test.expectedError == "" { + c.Assert(err, qt.IsNil) + c.Assert(res, qt.DeepEquals, test.expectedResult) + } else { + c.Assert(err, qt.ErrorMatches, test.expectedError) + } + }) + } +} + +func TestListServiceAccountCredentials(t *testing.T) { + c := qt.New(t) + + tests := []struct { + about string + getCloudCredential func(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) + getCloudCredentialAttributes func(ctx context.Context, u *openfga.User, cred *dbmodel.CloudCredential, hidden bool) (attrs map[string]string, redacted []string, err error) + ForEachUserCloudCredential func(ctx context.Context, u *dbmodel.Identity, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error + args params.ListServiceAccountCredentialsRequest + username string + addTuples []openfga.Tuple + expectedResult jujuparams.CredentialContentResults + expectedError string + }{{ + about: "Valid request", + ForEachUserCloudCredential: func(ctx context.Context, u *dbmodel.Identity, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error { + return nil }, - expectedError: "unauthorized", - args: params.UpdateServiceAccountCredentialsRequest{ + expectedResult: jujuparams.CredentialContentResults{ + Results: []jujuparams.CredentialContentResult{}}, + args: params.ListServiceAccountCredentialsRequest{ ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", }, - username: "alice", - }, { - about: "Invalid Client ID", - updateCloudCredential: func(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) { - return nil, nil + getCloudCredential: func(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) { + cred := &dbmodel.CloudCredential{} + return cred, nil }, - username: "alice", - args: params.UpdateServiceAccountCredentialsRequest{ - ClientID: "_123_", + getCloudCredentialAttributes: func(ctx context.Context, u *openfga.User, cred *dbmodel.CloudCredential, hidden bool) (attrs map[string]string, redacted []string, err error) { + return nil, nil, nil }, - expectedError: "invalid client ID", + username: "alice", + addTuples: []openfga.Tuple{{ + Object: ofganames.ConvertTag(names.NewUserTag("alice")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), + }}, }} for _, test := range tests { @@ -182,9 +301,11 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { err = pgDb.Migrate(context.Background(), false) c.Assert(err, qt.IsNil) jimm := &jimmtest.JIMM{ - AuthorizationClient_: func() *openfga.OFGAClient { return ofgaClient }, - UpdateCloudCredential_: test.updateCloudCredential, - DB_: func() *db.Database { return &pgDb }, + AuthorizationClient_: func() *openfga.OFGAClient { return ofgaClient }, + GetCloudCredential_: test.getCloudCredential, + GetCloudCredentialAttributes_: test.getCloudCredentialAttributes, + ForEachUserCloudCredential_: test.ForEachUserCloudCredential, + DB_: func() *db.Database { return &pgDb }, } var u dbmodel.Identity u.SetTag(names.NewUserTag(test.username)) @@ -196,7 +317,7 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { ofgaClient.AddRelation(context.Background(), test.addTuples...) } - res, err := cr.UpdateServiceAccountCredentials(context.Background(), test.args) + res, err := cr.ListServiceAccountCredentials(context.Background(), test.args) if test.expectedError == "" { c.Assert(err, qt.IsNil) c.Assert(res, qt.DeepEquals, test.expectedResult) diff --git a/internal/openfga/user.go b/internal/openfga/user.go index 9de3d3f6c..aab1bcbfd 100644 --- a/internal/openfga/user.go +++ b/internal/openfga/user.go @@ -13,6 +13,7 @@ import ( "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" ofganames "github.com/canonical/jimm/internal/openfga/names" + jimmnames "github.com/canonical/jimm/pkg/names" "github.com/canonical/ofga" ) @@ -79,6 +80,15 @@ func (u *User) IsModelWriter(ctx context.Context, resource names.ModelTag) (bool return isWriter, nil } +// IsServiceAccountAdmin returns true if user has administrator relation to the service account. +func (u *User) IsServiceAccountAdmin(ctx context.Context, clientID jimmnames.ServiceAccountTag) (bool, error) { + isAdmin, err := checkRelation(ctx, u, clientID, ofganames.AdministratorRelation) + if err != nil { + return false, errors.E(err) + } + return isAdmin, nil +} + // GetCloudAccess returns the relation the user has to the specified cloud. func (u *User) GetCloudAccess(ctx context.Context, resource names.CloudTag) Relation { isCloudAdmin, err := IsAdministrator(ctx, u, resource) From f70ba7157e7f1a117dbb30a706bffb07fab0d7fa Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:34:04 +0200 Subject: [PATCH 043/126] CSS-6766 implement grant access to service account (#1138) * Implement GrantServiceAccountAccess * Added godoc * Fixed test --- api/params/params.go | 10 +++ internal/jimm/service_account.go | 24 +++++ internal/jimm/service_account_test.go | 78 +++++++++++++++++ internal/jimmtest/jimm_mock.go | 11 +++ internal/jujuapi/access_control.go | 2 +- internal/jujuapi/access_control_test.go | 2 +- internal/jujuapi/controllerroot.go | 3 + internal/jujuapi/service_account.go | 30 +++++++ internal/jujuapi/service_account_test.go | 107 +++++++++++++++++++++++ internal/openfga/user.go | 2 +- 10 files changed, 266 insertions(+), 3 deletions(-) diff --git a/api/params/params.go b/api/params/params.go index a929cc717..83add83f3 100644 --- a/api/params/params.go +++ b/api/params/params.go @@ -417,3 +417,13 @@ type ListServiceAccountCredentialsRequest struct { // ClientID holds the client id of the service account. ClientID string `json:"client-id"` } + +// ListServiceAccountCredentialsRequest holds a request to list +// a service accounts cloud credentials. +type GrantServiceAccountAccess struct { + // Entities holds a slice of entities (identities and groups) + // that should have administration access to the desired clientID. + Entities []string `json:"entities"` + // ClientID holds the client id of the service account. + ClientID string `json:"client-id"` +} diff --git a/internal/jimm/service_account.go b/internal/jimm/service_account.go index 4a2bce026..0d431efde 100644 --- a/internal/jimm/service_account.go +++ b/internal/jimm/service_account.go @@ -9,6 +9,8 @@ import ( "github.com/canonical/jimm/internal/openfga" ofganames "github.com/canonical/jimm/internal/openfga/names" jimmnames "github.com/canonical/jimm/pkg/names" + "github.com/juju/zaputil/zapctx" + "go.uber.org/zap" ) // AddServiceAccount checks that no one owns the service account yet @@ -51,3 +53,25 @@ func (j *JIMM) AddServiceAccount(ctx context.Context, u *openfga.User, clientId } return nil } + +// GrantServiceAccountAccess creates an administrator relation between the tags provided +// and the service account. The provided tags must be users or groups (with the member relation) +// otherwise OpenFGA will report an error. +func (j *JIMM) GrantServiceAccountAccess(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error { + op := errors.Op("jimm.GrantServiceAccountAccess") + tuples := make([]openfga.Tuple, 0, len(tags)) + for _, tag := range tags { + tuple := openfga.Tuple{ + Object: tag, + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(svcAccTag), + } + tuples = append(tuples, tuple) + } + err := j.AuthorizationClient().AddRelation(ctx, tuples...) + if err != nil { + zapctx.Error(ctx, "failed to add tuple(s)", zap.NamedError("add-relation-error", err)) + return errors.E(op, errors.CodeOpenFGARequestFailed, err) + } + return nil +} diff --git a/internal/jimm/service_account_test.go b/internal/jimm/service_account_test.go index 924696d60..1ffb238fe 100644 --- a/internal/jimm/service_account_test.go +++ b/internal/jimm/service_account_test.go @@ -7,11 +7,16 @@ import ( "testing" qt "github.com/frankban/quicktest" + "github.com/juju/names/v4" + "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/jimmtest" "github.com/canonical/jimm/internal/openfga" + ofganames "github.com/canonical/jimm/internal/openfga/names" + jimmnames "github.com/canonical/jimm/pkg/names" + "github.com/canonical/ofga" ) func TestAddServiceAccount(t *testing.T) { @@ -46,3 +51,76 @@ func TestAddServiceAccount(t *testing.T) { err = j.AddServiceAccount(ctx, userAlice, clientID) c.Assert(err, qt.ErrorMatches, "service account already owned") } + +func TestGrantServiceAccountAccess(t *testing.T) { + c := qt.New(t) + + tests := []struct { + about string + grantServiceAccountAccess func(ctx context.Context, user *openfga.User, tags []string) error + clientID string + tags []*ofganames.Tag + username string + expectedError string + }{{ + about: "Valid request", + grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, tags []string) error { + return nil + }, + tags: []*ofganames.Tag{ + &ofga.Entity{ + Kind: "user", + ID: "alice", + }, + &ofga.Entity{ + Kind: "user", + ID: "bob", + }, + &ofga.Entity{ + Kind: "group", + ID: "1", + Relation: "member", + }, + }, + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + username: "alice", + }} + + for _, test := range tests { + test := test + c.Run(test.about, func(c *qt.C) { + ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) + c.Assert(err, qt.IsNil) + pgDb := db.Database{ + DB: jimmtest.PostgresDB(c, nil), + } + err = pgDb.Migrate(context.Background(), false) + c.Assert(err, qt.IsNil) + jimm := &jimm.JIMM{ + Database: pgDb, + OpenFGAClient: ofgaClient, + } + var u dbmodel.Identity + u.SetTag(names.NewUserTag(test.clientID)) + svcAccountIdentity := openfga.NewUser(&u, ofgaClient) + svcAccountTag := jimmnames.NewServiceAccountTag(test.clientID) + + err = jimm.GrantServiceAccountAccess(context.Background(), svcAccountIdentity, svcAccountTag, test.tags) + if test.expectedError == "" { + c.Assert(err, qt.IsNil) + for _, tag := range test.tags { + tuple := openfga.Tuple{ + Object: tag, + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(test.clientID)), + } + ok, err := jimm.AuthorizationClient().CheckRelation(context.Background(), tuple, false) + c.Assert(err, qt.IsNil) + c.Assert(ok, qt.IsTrue) + } + } else { + c.Assert(err, qt.ErrorMatches, test.expectedError) + } + }) + } +} diff --git a/internal/jimmtest/jimm_mock.go b/internal/jimmtest/jimm_mock.go index f5587c0d9..e5d5b71c4 100644 --- a/internal/jimmtest/jimm_mock.go +++ b/internal/jimmtest/jimm_mock.go @@ -18,7 +18,9 @@ import ( "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/openfga" + ofganames "github.com/canonical/jimm/internal/openfga/names" "github.com/canonical/jimm/internal/pubsub" + jimmnames "github.com/canonical/jimm/pkg/names" ) // JIMM is a default implementation of the jujuapi.JIMM interface. Every method @@ -64,6 +66,7 @@ type JIMM struct { GrantCloudAccess_ func(ctx context.Context, user *openfga.User, ct names.CloudTag, ut names.UserTag, access string) error GrantModelAccess_ func(ctx context.Context, user *openfga.User, mt names.ModelTag, ut names.UserTag, access jujuparams.UserAccessPermission) error GrantOfferAccess_ func(ctx context.Context, u *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) error + GrantServiceAccountAccess_ func(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error ImportModel_ func(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error IdentityModelDefaults_ func(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) InitiateMigration_ func(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec, targetControllerID uint) (jujuparams.InitiateMigrationResult, error) @@ -330,6 +333,14 @@ func (j *JIMM) GrantOfferAccess(ctx context.Context, u *openfga.User, offerURL s } return j.GrantOfferAccess_(ctx, u, offerURL, ut, access) } + +func (j *JIMM) GrantServiceAccountAccess(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error { + if j.GrantServiceAccountAccess_ == nil { + return errors.E(errors.CodeNotImplemented) + } + return j.GrantServiceAccountAccess_(ctx, u, svcAccTag, tags) +} + func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error { if j.ImportModel_ == nil { return errors.E(errors.CodeNotImplemented) diff --git a/internal/jujuapi/access_control.go b/internal/jujuapi/access_control.go index 85117f604..2eda91274 100644 --- a/internal/jujuapi/access_control.go +++ b/internal/jujuapi/access_control.go @@ -205,7 +205,7 @@ func resolveTag(jimmUUID string, db *db.Database, tag string) (*ofganames.Tag, e } err := db.GetGroup(ctx, entry) if err != nil { - return nil, errors.E("group not found") + return nil, errors.E(fmt.Sprintf("group %s not found", trailer)) } return ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(strconv.FormatUint(uint64(entry.ID), 10)), relation), nil diff --git a/internal/jujuapi/access_control_test.go b/internal/jujuapi/access_control_test.go index f29309f2e..a03be88c3 100644 --- a/internal/jujuapi/access_control_test.go +++ b/internal/jujuapi/access_control_test.go @@ -1334,7 +1334,7 @@ func (s *accessControlSuite) TestResolveTupleObjectHandlesErrors(c *gc.C) { // Resolves bad groups where they do not exist { input: "group-myspecialpokemon-his-name-is-youguessedit-diglett", - want: "group not found", + want: "group myspecialpokemon-his-name-is-youguessedit-diglett not found", }, // Resolves bad controllers where they do not exist { diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index fd3a09578..19ef2a9f2 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -20,7 +20,9 @@ import ( "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/jujuapi/rpc" "github.com/canonical/jimm/internal/openfga" + ofganames "github.com/canonical/jimm/internal/openfga/names" "github.com/canonical/jimm/internal/pubsub" + jimmnames "github.com/canonical/jimm/pkg/names" ) type JIMM interface { @@ -61,6 +63,7 @@ type JIMM interface { GrantCloudAccess(ctx context.Context, user *openfga.User, ct names.CloudTag, ut names.UserTag, access string) error GrantModelAccess(ctx context.Context, user *openfga.User, mt names.ModelTag, ut names.UserTag, access jujuparams.UserAccessPermission) error GrantOfferAccess(ctx context.Context, u *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) error + GrantServiceAccountAccess(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error IdentityModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) ImportModel(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error InitiateInternalMigration(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetController string) (jujuparams.InitiateMigrationResult, error) diff --git a/internal/jujuapi/service_account.go b/internal/jujuapi/service_account.go index c036aa9bd..7499f3746 100644 --- a/internal/jujuapi/service_account.go +++ b/internal/jujuapi/service_account.go @@ -13,6 +13,7 @@ import ( "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/openfga" + ofganames "github.com/canonical/jimm/internal/openfga/names" jimmnames "github.com/canonical/jimm/pkg/names" ) @@ -103,3 +104,32 @@ func (r *controllerRoot) ListServiceAccountCredentials(ctx context.Context, req } return getIdentityCredentials(ctx, targetIdentity, r.jimm, req.CloudCredentialArgs) } + +// GrantServiceAccountAccess is the method handler for granting new users/groups with access +// to service accounts. +func (r *controllerRoot) GrantServiceAccountAccess(ctx context.Context, req apiparams.GrantServiceAccountAccess) error { + const op = errors.Op("jujuapi.GrantServiceAccountAccess") + + targetUser, err := r.getServiceAccount(ctx, req.ClientID) + if err != nil { + return errors.E(op, err) + } + tags := make([]*ofganames.Tag, 0, len(req.Entities)) + // Validate tags + for _, val := range req.Entities { + tag, err := parseTag(ctx, r.jimm.ResourceTag().Id(), r.jimm.DB(), val) + if err != nil { + return errors.E(op, err) + } + if tag.Kind != openfga.UserType && tag.Kind != openfga.GroupType { + return errors.E(op, "invalid entity - not user or group") + } + if tag.Kind == openfga.GroupType { + tag.Relation = ofganames.MemberRelation + } + tags = append(tags, tag) + } + svcAccTag := jimmnames.NewServiceAccountTag(req.ClientID) + + return r.jimm.GrantServiceAccountAccess(ctx, targetUser, svcAccTag, tags) +} diff --git a/internal/jujuapi/service_account_test.go b/internal/jujuapi/service_account_test.go index 8e7f77790..383c97214 100644 --- a/internal/jujuapi/service_account_test.go +++ b/internal/jujuapi/service_account_test.go @@ -328,6 +328,113 @@ func TestListServiceAccountCredentials(t *testing.T) { } } +func TestGrantServiceAccountAccess(t *testing.T) { + c := qt.New(t) + + tests := []struct { + about string + grantServiceAccountAccess func(ctx context.Context, user *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error + params params.GrantServiceAccountAccess + tags []string + username string + addTuples []openfga.Tuple + expectedError string + }{{ + about: "Valid request", + grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error { + return nil + }, + params: params.GrantServiceAccountAccess{ + Entities: []string{ + "user-alice", + "user-bob", + }, + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + }, + username: "alice", + addTuples: []openfga.Tuple{{ + Object: ofganames.ConvertTag(names.NewUserTag("alice")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), + }}, + }, { + about: "Group that doesn't exist", + grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error { + return nil + }, + params: params.GrantServiceAccountAccess{ + Entities: []string{ + "user-alice", + "user-bob", + // This group doesn't exist. + "group-bar", + }, + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + }, + username: "alice", + addTuples: []openfga.Tuple{{ + Object: ofganames.ConvertTag(names.NewUserTag("alice")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), + }}, + expectedError: "group bar not found", + }, { + about: "Invalid tags", + grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error { + return nil + }, + params: params.GrantServiceAccountAccess{ + Entities: []string{ + "user-alice", + "user-bob", + "controller-jimm", + }, + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + }, + username: "alice", + addTuples: []openfga.Tuple{{ + Object: ofganames.ConvertTag(names.NewUserTag("alice")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), + }}, + expectedError: "invalid entity - not user or group", + }} + + for _, test := range tests { + test := test + c.Run(test.about, func(c *qt.C) { + ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) + c.Assert(err, qt.IsNil) + pgDb := db.Database{ + DB: jimmtest.PostgresDB(c, nil), + } + err = pgDb.Migrate(context.Background(), false) + c.Assert(err, qt.IsNil) + jimm := &jimmtest.JIMM{ + AuthorizationClient_: func() *openfga.OFGAClient { return ofgaClient }, + GrantServiceAccountAccess_: test.grantServiceAccountAccess, + DB_: func() *db.Database { return &pgDb }, + } + var u dbmodel.Identity + u.SetTag(names.NewUserTag(test.username)) + user := openfga.NewUser(&u, ofgaClient) + cr := jujuapi.NewControllerRoot(jimm, jujuapi.Params{}) + jujuapi.SetUser(cr, user) + + if len(test.addTuples) > 0 { + ofgaClient.AddRelation(context.Background(), test.addTuples...) + } + + err = cr.GrantServiceAccountAccess(context.Background(), test.params) + if test.expectedError == "" { + c.Assert(err, qt.IsNil) + } else { + c.Assert(err, qt.ErrorMatches, test.expectedError) + } + }) + } +} + // Integration tests below. type serviceAccountSuite struct { websocketSuite diff --git a/internal/openfga/user.go b/internal/openfga/user.go index aab1bcbfd..dbb5e4c24 100644 --- a/internal/openfga/user.go +++ b/internal/openfga/user.go @@ -80,7 +80,7 @@ func (u *User) IsModelWriter(ctx context.Context, resource names.ModelTag) (bool return isWriter, nil } -// IsServiceAccountAdmin returns true if user has administrator relation to the service account. +// IsServiceAccountAdmin returns true if the user has administrator relation to the service account. func (u *User) IsServiceAccountAdmin(ctx context.Context, clientID jimmnames.ServiceAccountTag) (bool, error) { isAdmin, err := checkRelation(ctx, u, clientID, ofganames.AdministratorRelation) if err != nil { From 984fc2193ec1526db98af3c6854493548ff3373a Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Mon, 29 Jan 2024 09:53:25 +0200 Subject: [PATCH 044/126] Base of service account plugin (#1140) * Start of service account plugin * Start of service account plugin --- cmd/service-accounts/main.go | 36 +++++++++++++++++++++++++++++ internal/jujuapi/service_account.go | 5 ++-- 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 cmd/service-accounts/main.go diff --git a/cmd/service-accounts/main.go b/cmd/service-accounts/main.go new file mode 100644 index 000000000..a57fc45eb --- /dev/null +++ b/cmd/service-accounts/main.go @@ -0,0 +1,36 @@ +// Copyright 2021 Canonical Ltd. + +package main + +import ( + "fmt" + "os" + + jujucmd "github.com/juju/cmd/v3" +) + +var serviceAccountDoc = ` +juju service-accounts enables users to manage service accounts. +` + +func NewSuperCommand() *jujucmd.SuperCommand { + serviceAccountCmd := jujucmd.NewSuperCommand(jujucmd.SuperCommandParams{ + Name: "service-accounts", + Doc: serviceAccountDoc, + }) + // Register commands here: + // serviceAccountCmd.Register(cmd.NewCommand()) + return serviceAccountCmd +} + +func main() { + ctx, err := jujucmd.DefaultContext() + if err != nil { + fmt.Printf("failed to get command context: %v\n", err) + os.Exit(2) + } + superCmd := NewSuperCommand() + args := os.Args + + os.Exit(jujucmd.Main(superCmd, ctx, args[1:])) +} diff --git a/internal/jujuapi/service_account.go b/internal/jujuapi/service_account.go index 7499f3746..ac3419b78 100644 --- a/internal/jujuapi/service_account.go +++ b/internal/jujuapi/service_account.go @@ -21,7 +21,7 @@ import ( // AddGroup creates a group within JIMMs DB for reference by OpenFGA. func (r *controllerRoot) AddServiceAccount(ctx context.Context, req apiparams.AddServiceAccountRequest) error { - const op = errors.Op("jujuapi.AddGroup") + const op = errors.Op("jujuapi.AddServiceAccount") if !jimmnames.IsValidServiceAccountId(req.ClientID) { return errors.E(op, errors.CodeBadRequest, "invalid client ID") @@ -95,8 +95,9 @@ func (r *controllerRoot) UpdateServiceAccountCredentials(ctx context.Context, re return results, nil } +// ListServiceAccountCredentials lists the cloud credentials available for a service account. func (r *controllerRoot) ListServiceAccountCredentials(ctx context.Context, req apiparams.ListServiceAccountCredentialsRequest) (jujuparams.CredentialContentResults, error) { - const op = errors.Op("jujuapi.UpdateServiceAccountCredentials") + const op = errors.Op("jujuapi.ListServiceAccountCredentials") targetIdentity, err := r.getServiceAccount(ctx, req.ClientID) if err != nil { From 31e87ee3d0c8774bed2cda258d3e980147bebb7d Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:49:49 +0200 Subject: [PATCH 045/126] CSS-6952 add service account command (#1141) * Add CLI command and tests for addServiceAccount * Hook up GoCheck to test runner * Test for addServiceAccount CLI method * Extend test * PR comments * PR comments updated godoc --- api/jimm.go | 5 + cmd/jimmctl/cmd/addcloudtocontroller_test.go | 7 +- cmd/jimmctl/cmd/addcontroller_test.go | 7 +- cmd/jimmctl/cmd/controllerinfo_test.go | 3 +- cmd/jimmctl/cmd/crossmodelquery_test.go | 5 +- cmd/jimmctl/cmd/grantauditlogaccess_test.go | 7 +- cmd/jimmctl/cmd/group_test.go | 33 +++--- .../cmd/importcloudcredentials_test.go | 5 +- cmd/jimmctl/cmd/importmodel_test.go | 17 +-- cmd/jimmctl/cmd/listauditevents_test.go | 7 +- cmd/jimmctl/cmd/listcontrollers_test.go | 7 +- cmd/jimmctl/cmd/migratemodel_test.go | 9 +- cmd/jimmctl/cmd/modelstatus_test.go | 7 +- cmd/jimmctl/cmd/purge_logs_test.go | 16 +-- cmd/jimmctl/cmd/relation_test.go | 39 +++---- .../cmd/removecloudfromcontroller_test.go | 11 +- cmd/jimmctl/cmd/removecontroller_test.go | 7 +- cmd/jimmctl/cmd/revokeauditlogaccess_test.go | 7 +- .../cmd/setcontrollerdeprecated_test.go | 7 +- cmd/jimmctl/cmd/updatemigratedmodel_test.go | 15 +-- cmd/serviceaccounts/cmd/addserviceaccount.go | 100 ++++++++++++++++++ .../cmd/addserviceaccount_test.go | 47 ++++++++ cmd/serviceaccounts/cmd/export_test.go | 23 ++++ cmd/serviceaccounts/cmd/package_test.go | 13 +++ .../main.go | 3 +- .../cmdtest/jimmsuite.go | 20 ++-- internal/jimm/service_account.go | 1 + internal/jujuapi/jimm.go | 4 + 28 files changed, 324 insertions(+), 108 deletions(-) create mode 100644 cmd/serviceaccounts/cmd/addserviceaccount.go create mode 100644 cmd/serviceaccounts/cmd/addserviceaccount_test.go create mode 100644 cmd/serviceaccounts/cmd/export_test.go create mode 100644 cmd/serviceaccounts/cmd/package_test.go rename cmd/{service-accounts => serviceaccounts}/main.go (85%) rename cmd/jimmctl/cmd/jimmsuite_test.go => internal/cmdtest/jimmsuite.go (89%) diff --git a/api/jimm.go b/api/jimm.go index be08b1333..a98573801 100644 --- a/api/jimm.go +++ b/api/jimm.go @@ -190,3 +190,8 @@ func (c *Client) MigrateModel(req *params.MigrateModelRequest) (*jujuparams.Init err := c.caller.APICall("JIMM", 4, "", "MigrateModel", req, &response) return &response, err } + +// AddServiceAccount binds a service account to a user allowing them to manage it. +func (c *Client) AddServiceAccount(req *params.AddServiceAccountRequest) error { + return c.caller.APICall("JIMM", 4, "", "AddServiceAccount", req, nil) +} diff --git a/cmd/jimmctl/cmd/addcloudtocontroller_test.go b/cmd/jimmctl/cmd/addcloudtocontroller_test.go index e99d92393..98e59d71f 100644 --- a/cmd/jimmctl/cmd/addcloudtocontroller_test.go +++ b/cmd/jimmctl/cmd/addcloudtocontroller_test.go @@ -15,6 +15,7 @@ import ( gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/openfga" @@ -22,13 +23,13 @@ import ( ) type addCloudToControllerSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&addCloudToControllerSuite{}) func (s *addCloudToControllerSuite) SetUpTest(c *gc.C) { - s.jimmSuite.SetUpTest(c) + s.JimmCmdSuite.SetUpTest(c) // We add user bob, who is a JIMM administrator. err := s.JIMM.Database.UpdateIdentity(context.Background(), &dbmodel.Identity{ @@ -166,7 +167,7 @@ clouds: c.Log(test.about) tmpfile, cleanupFunc := writeTempFile(c, test.cloudInfo) - bClient := s.userBakeryClient("bob@external") + bClient := s.UserBakeryClient("bob@external") // Running the command succeeds newCmd := cmd.NewAddCloudToControllerCommandForTesting(s.ClientStore(), bClient, test.cloudByNameFunc) var err error diff --git a/cmd/jimmctl/cmd/addcontroller_test.go b/cmd/jimmctl/cmd/addcontroller_test.go index cba5847f6..b142d5668 100644 --- a/cmd/jimmctl/cmd/addcontroller_test.go +++ b/cmd/jimmctl/cmd/addcontroller_test.go @@ -14,11 +14,12 @@ import ( apiparams "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/jimmtest" ) type addControllerSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&addControllerSuite{}) @@ -37,7 +38,7 @@ func (s *addControllerSuite) TestAddControllerSuperuser(c *gc.C) { defer os.RemoveAll(tmpdir) // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") ctx, err := cmdtesting.RunCommand(c, cmd.NewAddControllerCommandForTesting(s.ClientStore(), bClient), tmpfile) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(ctx), gc.Matches, `name: controller-1 @@ -100,7 +101,7 @@ func (s *addControllerSuite) TestAddController(c *gc.C) { defer os.RemoveAll(tmpdir) // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewAddControllerCommandForTesting(s.ClientStore(), bClient), tmpfile) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/controllerinfo_test.go b/cmd/jimmctl/cmd/controllerinfo_test.go index b93322143..e7d46ebc9 100644 --- a/cmd/jimmctl/cmd/controllerinfo_test.go +++ b/cmd/jimmctl/cmd/controllerinfo_test.go @@ -11,10 +11,11 @@ import ( gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" ) type controllerInfoSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&controllerInfoSuite{}) diff --git a/cmd/jimmctl/cmd/crossmodelquery_test.go b/cmd/jimmctl/cmd/crossmodelquery_test.go index eb73d3129..2e27f5d73 100644 --- a/cmd/jimmctl/cmd/crossmodelquery_test.go +++ b/cmd/jimmctl/cmd/crossmodelquery_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/jimmtest" "github.com/juju/cmd/v3/cmdtesting" jujuparams "github.com/juju/juju/rpc/params" @@ -15,7 +16,7 @@ import ( ) type crossModelQuerySuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&crossModelQuerySuite{}) @@ -23,7 +24,7 @@ var _ = gc.Suite(&crossModelQuerySuite{}) func (s *crossModelQuerySuite) TestCrossModelQueryCommand(c *gc.C) { // Test setup. store := s.ClientStore() - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") s.AddController(c, "controller-2", s.APIInfo(c)) cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/alice@external/cred") diff --git a/cmd/jimmctl/cmd/grantauditlogaccess_test.go b/cmd/jimmctl/cmd/grantauditlogaccess_test.go index 288b08f30..6036df171 100644 --- a/cmd/jimmctl/cmd/grantauditlogaccess_test.go +++ b/cmd/jimmctl/cmd/grantauditlogaccess_test.go @@ -7,10 +7,11 @@ import ( gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" ) type grantAuditLogAccessSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } // TODO (alesstimec) uncomment once granting/revoking is reimplemented @@ -18,14 +19,14 @@ type grantAuditLogAccessSuite struct { func (s *grantAuditLogAccessSuite) TestGrantAuditLogAccessSuperuser(c *gc.C) { // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") _, err := cmdtesting.RunCommand(c, cmd.NewGrantAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@external") c.Assert(err, gc.IsNil) } func (s *grantAuditLogAccessSuite) TestGrantAuditLogAccess(c *gc.C) { // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewGrantAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@external") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/group_test.go b/cmd/jimmctl/cmd/group_test.go index ddfd63d6a..ce3b02427 100644 --- a/cmd/jimmctl/cmd/group_test.go +++ b/cmd/jimmctl/cmd/group_test.go @@ -11,23 +11,24 @@ import ( gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" ) type groupSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&groupSuite{}) func (s *groupSuite) TestAddGroupSuperuser(c *gc.C) { // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") _, err := cmdtesting.RunCommand(c, cmd.NewAddGroupCommandForTesting(s.ClientStore(), bClient), "test-group") c.Assert(err, gc.IsNil) group := &dbmodel.GroupEntry{Name: "test-group"} - err = s.jimmSuite.JIMM.Database.GetGroup(context.TODO(), group) + err = s.JimmCmdSuite.JIMM.Database.GetGroup(context.TODO(), group) c.Assert(err, gc.IsNil) c.Assert(group.ID, gc.Equals, uint(1)) c.Assert(group.Name, gc.Equals, "test-group") @@ -35,23 +36,23 @@ func (s *groupSuite) TestAddGroupSuperuser(c *gc.C) { func (s *groupSuite) TestAddGroup(c *gc.C) { // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewAddGroupCommandForTesting(s.ClientStore(), bClient), "test-group") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *groupSuite) TestRenameGroupSuperuser(c *gc.C) { // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") - err := s.jimmSuite.JIMM.Database.AddGroup(context.TODO(), "test-group") + err := s.JimmCmdSuite.JIMM.Database.AddGroup(context.TODO(), "test-group") c.Assert(err, gc.IsNil) _, err = cmdtesting.RunCommand(c, cmd.NewRenameGroupCommandForTesting(s.ClientStore(), bClient), "test-group", "renamed-group") c.Assert(err, gc.IsNil) group := &dbmodel.GroupEntry{Name: "renamed-group"} - err = s.jimmSuite.JIMM.Database.GetGroup(context.TODO(), group) + err = s.JimmCmdSuite.JIMM.Database.GetGroup(context.TODO(), group) c.Assert(err, gc.IsNil) c.Assert(group.ID, gc.Equals, uint(1)) c.Assert(group.Name, gc.Equals, "renamed-group") @@ -59,29 +60,29 @@ func (s *groupSuite) TestRenameGroupSuperuser(c *gc.C) { func (s *groupSuite) TestRenameGroup(c *gc.C) { // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewRenameGroupCommandForTesting(s.ClientStore(), bClient), "test-group", "renamed-group") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *groupSuite) TestRemoveGroupSuperuser(c *gc.C) { // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") - err := s.jimmSuite.JIMM.Database.AddGroup(context.TODO(), "test-group") + err := s.JimmCmdSuite.JIMM.Database.AddGroup(context.TODO(), "test-group") c.Assert(err, gc.IsNil) _, err = cmdtesting.RunCommand(c, cmd.NewRemoveGroupCommandForTesting(s.ClientStore(), bClient), "test-group", "-y") c.Assert(err, gc.IsNil) group := &dbmodel.GroupEntry{Name: "test-group"} - err = s.jimmSuite.JIMM.Database.GetGroup(context.TODO(), group) + err = s.JimmCmdSuite.JIMM.Database.GetGroup(context.TODO(), group) c.Assert(err, gc.ErrorMatches, "record not found") } func (s *groupSuite) TestRemoveGroupWithoutFlag(c *gc.C) { // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") _, err := cmdtesting.RunCommand(c, cmd.NewRemoveGroupCommandForTesting(s.ClientStore(), bClient), "test-group") c.Assert(err.Error(), gc.Matches, "Failed to read from input.") @@ -89,17 +90,17 @@ func (s *groupSuite) TestRemoveGroupWithoutFlag(c *gc.C) { func (s *groupSuite) TestRemoveGroup(c *gc.C) { // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewRemoveGroupCommandForTesting(s.ClientStore(), bClient), "test-group", "-y") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *groupSuite) TestListGroupsSuperuser(c *gc.C) { // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") for i := 0; i < 3; i++ { - err := s.jimmSuite.JIMM.Database.AddGroup(context.TODO(), fmt.Sprint("test-group", i)) + err := s.JimmCmdSuite.JIMM.Database.AddGroup(context.TODO(), fmt.Sprint("test-group", i)) c.Assert(err, gc.IsNil) } @@ -113,7 +114,7 @@ func (s *groupSuite) TestListGroupsSuperuser(c *gc.C) { func (s *groupSuite) TestListGroups(c *gc.C) { // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewListGroupsCommandForTesting(s.ClientStore(), bClient), "test-group") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/importcloudcredentials_test.go b/cmd/jimmctl/cmd/importcloudcredentials_test.go index 69407397e..2dea10c8b 100644 --- a/cmd/jimmctl/cmd/importcloudcredentials_test.go +++ b/cmd/jimmctl/cmd/importcloudcredentials_test.go @@ -11,11 +11,12 @@ import ( gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" ) type importCloudCredentialsSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&importCloudCredentialsSuite{}) @@ -62,7 +63,7 @@ func (s *importCloudCredentialsSuite) TestImportCloudCredentials(c *gc.C) { c.Assert(err, gc.IsNil) // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") _, err = cmdtesting.RunCommand(c, cmd.NewImportCloudCredentialsCommandForTesting(s.ClientStore(), bClient), tmpfile) c.Assert(err, gc.IsNil) diff --git a/cmd/jimmctl/cmd/importmodel_test.go b/cmd/jimmctl/cmd/importmodel_test.go index fcea71caa..e034b056d 100644 --- a/cmd/jimmctl/cmd/importmodel_test.go +++ b/cmd/jimmctl/cmd/importmodel_test.go @@ -13,12 +13,13 @@ import ( gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/jimmtest" ) type importModelSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&importModelSuite{}) @@ -42,7 +43,7 @@ func (s *importModelSuite) TestImportModelSuperuser(c *gc.C) { defer m.Close() // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") _, err = cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-1", m.ModelUUID()) c.Assert(err, gc.IsNil) @@ -69,7 +70,7 @@ func (s *importModelSuite) TestImportModelFromLocalUser(c *gc.C) { c.Assert(err, gc.Equals, nil) // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") _, err = cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-1", mt.Id(), "--owner", "alice@external") c.Assert(err, gc.IsNil) @@ -100,31 +101,31 @@ func (s *importModelSuite) TestImportModelUnauthorized(c *gc.C) { defer m.Close() // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err = cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-1", m.ModelUUID()) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *importModelSuite) TestImportModelNoController(c *gc.C) { - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.ErrorMatches, `controller not specified`) } func (s *importModelSuite) TestImportModelNoModelUUID(c *gc.C) { - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-id") c.Assert(err, gc.ErrorMatches, `model uuid not specified`) } func (s *importModelSuite) TestImportModelInvalidModelUUID(c *gc.C) { - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-id", "not-a-uuid") c.Assert(err, gc.ErrorMatches, `invalid model uuid`) } func (s *importModelSuite) TestImportModelTooManyArgs(c *gc.C) { - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-id", "not-a-uuid", "spare-argument") c.Assert(err, gc.ErrorMatches, `too many args`) } diff --git a/cmd/jimmctl/cmd/listauditevents_test.go b/cmd/jimmctl/cmd/listauditevents_test.go index a16411dfa..065060f2a 100644 --- a/cmd/jimmctl/cmd/listauditevents_test.go +++ b/cmd/jimmctl/cmd/listauditevents_test.go @@ -9,11 +9,12 @@ import ( gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/jimmtest" ) type listAuditEventsSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&listAuditEventsSuite{}) @@ -26,7 +27,7 @@ func (s *listAuditEventsSuite) TestListAuditEventsSuperuser(c *gc.C) { s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") context, err := cmdtesting.RunCommand(c, cmd.NewListAuditEventsCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, @@ -65,7 +66,7 @@ func (s *listAuditEventsSuite) TestListAuditEventsStatus(c *gc.C) { s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewListAuditEventsCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/listcontrollers_test.go b/cmd/jimmctl/cmd/listcontrollers_test.go index 061a5337e..fcb1b859a 100644 --- a/cmd/jimmctl/cmd/listcontrollers_test.go +++ b/cmd/jimmctl/cmd/listcontrollers_test.go @@ -7,6 +7,7 @@ import ( gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/jimmtest" ) @@ -67,7 +68,7 @@ var ( ) type listControllersSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&listControllersSuite{}) @@ -76,7 +77,7 @@ func (s *listControllersSuite) TestListControllersSuperuser(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") context, err := cmdtesting.RunCommand(c, cmd.NewListControllersCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, expectedSuperuserOutput) @@ -86,7 +87,7 @@ func (s *listControllersSuite) TestListControllers(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") context, err := cmdtesting.RunCommand(c, cmd.NewListControllersCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, expectedOutput) diff --git a/cmd/jimmctl/cmd/migratemodel_test.go b/cmd/jimmctl/cmd/migratemodel_test.go index 479b6e161..9eaa1086a 100644 --- a/cmd/jimmctl/cmd/migratemodel_test.go +++ b/cmd/jimmctl/cmd/migratemodel_test.go @@ -9,11 +9,12 @@ import ( gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/jimmtest" ) type migrateModelSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&migrateModelSuite{}) @@ -46,7 +47,7 @@ func (s *migrateModelSuite) TestMigrateModelCommandSuperuser(c *gc.C) { mt2 := s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") context, err := cmdtesting.RunCommand(c, cmd.NewMigrateModelCommandForTesting(s.ClientStore(), bClient), "controller-1", mt.String(), mt2.String()) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, migrationResultRegex) @@ -60,13 +61,13 @@ func (s *migrateModelSuite) TestMigrateModelCommandFailsWithInvalidModelTag(c *g s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") _, err := cmdtesting.RunCommand(c, cmd.NewMigrateModelCommandForTesting(s.ClientStore(), bClient), "controller-1", "model-001", "model-002") c.Assert(err, gc.ErrorMatches, ".* is not a valid model tag") } func (s *migrateModelSuite) TestMigrateModelCommandFailsWithMissingArgs(c *gc.C) { - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") _, err := cmdtesting.RunCommand(c, cmd.NewMigrateModelCommandForTesting(s.ClientStore(), bClient), "myController") c.Assert(err, gc.ErrorMatches, "Missing controller and model tag arguments") } diff --git a/cmd/jimmctl/cmd/modelstatus_test.go b/cmd/jimmctl/cmd/modelstatus_test.go index c078b7d9e..27dfce70f 100644 --- a/cmd/jimmctl/cmd/modelstatus_test.go +++ b/cmd/jimmctl/cmd/modelstatus_test.go @@ -9,6 +9,7 @@ import ( gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/jimmtest" ) @@ -47,7 +48,7 @@ volumes: \[\] ) type modelStatusSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&modelStatusSuite{}) @@ -60,7 +61,7 @@ func (s *modelStatusSuite) TestModelStatusSuperuser(c *gc.C) { mt := s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") context, err := cmdtesting.RunCommand(c, cmd.NewModelStatusCommandForTesting(s.ClientStore(), bClient), mt.Id()) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, expectedModelStatusOutput) @@ -74,7 +75,7 @@ func (s *modelStatusSuite) TestModelStatus(c *gc.C) { mt := s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewModelStatusCommandForTesting(s.ClientStore(), bClient), mt.Id()) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/purge_logs_test.go b/cmd/jimmctl/cmd/purge_logs_test.go index 49268816a..1b20acf5f 100644 --- a/cmd/jimmctl/cmd/purge_logs_test.go +++ b/cmd/jimmctl/cmd/purge_logs_test.go @@ -5,22 +5,24 @@ import ( "context" "time" - "github.com/canonical/jimm/cmd/jimmctl/cmd" - "github.com/canonical/jimm/internal/dbmodel" "github.com/juju/cmd/v3/cmdtesting" "github.com/juju/names/v4" gc "gopkg.in/check.v1" + + "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" + "github.com/canonical/jimm/internal/dbmodel" ) type purgeLogsSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&purgeLogsSuite{}) func (s *purgeLogsSuite) TestPurgeLogsSuperuser(c *gc.C) { // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") datastring := "2021-01-01T00:00:00Z" cmdCtx, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), datastring) c.Assert(err, gc.IsNil) @@ -31,7 +33,7 @@ func (s *purgeLogsSuite) TestPurgeLogsSuperuser(c *gc.C) { func (s *purgeLogsSuite) TestInvalidISO8601Date(c *gc.C) { // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") datastring := "13/01/2021" _, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), datastring) c.Assert(err, gc.ErrorMatches, `invalid date. Expected ISO8601 date`) @@ -40,7 +42,7 @@ func (s *purgeLogsSuite) TestInvalidISO8601Date(c *gc.C) { func (s *purgeLogsSuite) TestPurgeLogs(c *gc.C) { // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), "2021-01-01T00:00:00Z") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } @@ -82,7 +84,7 @@ func (s *purgeLogsSuite) TestPurgeLogsFromDb(c *gc.C) { tomorrow := relativeNow.AddDate(0, 0, 1).Format(layout) //alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") cmdCtx, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), tomorrow) c.Assert(err, gc.IsNil) // check that logs have been deleted diff --git a/cmd/jimmctl/cmd/relation_test.go b/cmd/jimmctl/cmd/relation_test.go index be79fe3a0..7da41f007 100644 --- a/cmd/jimmctl/cmd/relation_test.go +++ b/cmd/jimmctl/cmd/relation_test.go @@ -22,6 +22,7 @@ import ( apiparams "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/openfga" @@ -30,14 +31,14 @@ import ( ) type relationSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&relationSuite{}) func (s *relationSuite) TestAddRelationSuperuser(c *gc.C) { // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") group1 := "testGroup1" group2 := "testGroup2" type tuple struct { @@ -81,9 +82,9 @@ func (s *relationSuite) TestAddRelationSuperuser(c *gc.C) { }, } - err := s.jimmSuite.JIMM.Database.AddGroup(context.Background(), group1) + err := s.JimmCmdSuite.JIMM.Database.AddGroup(context.Background(), group1) c.Assert(err, gc.IsNil) - err = s.jimmSuite.JIMM.Database.AddGroup(context.Background(), group2) + err = s.JimmCmdSuite.JIMM.Database.AddGroup(context.Background(), group2) c.Assert(err, gc.IsNil) for i, tc := range tests { @@ -94,7 +95,7 @@ func (s *relationSuite) TestAddRelationSuperuser(c *gc.C) { c.Assert(strings.Contains(err.Error(), tc.message), gc.Equals, true) } else { c.Assert(err, gc.IsNil) - tuples, ct, err := s.jimmSuite.JIMM.OpenFGAClient.ReadRelatedObjects(context.Background(), openfga.Tuple{}, 50, "") + tuples, ct, err := s.JimmCmdSuite.JIMM.OpenFGAClient.ReadRelatedObjects(context.Background(), openfga.Tuple{}, 50, "") c.Assert(err, gc.IsNil) c.Assert(ct, gc.Equals, "") // NOTE: this is a bad test because it relies on the number of related objects. So all the @@ -108,7 +109,7 @@ func (s *relationSuite) TestAddRelationSuperuser(c *gc.C) { func (s *relationSuite) TestMissingParamsAddRelationSuperuser(c *gc.C) { // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") _, err := cmdtesting.RunCommand(c, cmd.NewAddRelationCommandForTesting(s.ClientStore(), bClient), "foo", "bar") c.Assert(err, gc.ErrorMatches, "target object not specified") @@ -121,7 +122,7 @@ func (s *relationSuite) TestMissingParamsAddRelationSuperuser(c *gc.C) { func (s *relationSuite) TestAddRelationViaFileSuperuser(c *gc.C) { // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") group1 := "testGroup1" group2 := "testGroup2" group3 := "testGroup3" @@ -143,21 +144,21 @@ func (s *relationSuite) TestAddRelationViaFileSuperuser(c *gc.C) { _, err = cmdtesting.RunCommand(c, cmd.NewAddRelationCommandForTesting(s.ClientStore(), bClient), "-f", file.Name()) c.Assert(err, gc.IsNil) - tuples, ct, err := s.jimmSuite.JIMM.OpenFGAClient.ReadRelatedObjects(context.Background(), openfga.Tuple{}, 50, "") + tuples, ct, err := s.JimmCmdSuite.JIMM.OpenFGAClient.ReadRelatedObjects(context.Background(), openfga.Tuple{}, 50, "") c.Assert(err, gc.IsNil) c.Assert(ct, gc.Equals, "") c.Assert(len(tuples), gc.Equals, 4) } func (s *relationSuite) TestAddRelationRejectsUnauthorisedUsers(c *gc.C) { - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewAddRelationCommandForTesting(s.ClientStore(), bClient), "test-group1", "member", "test-group2") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *relationSuite) TestRemoveRelationSuperuser(c *gc.C) { // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") group1 := "testGroup1" group2 := "testGroup2" type tuple struct { @@ -175,9 +176,9 @@ func (s *relationSuite) TestRemoveRelationSuperuser(c *gc.C) { } //Create groups and relation - err := s.jimmSuite.JIMM.Database.AddGroup(context.Background(), group1) + err := s.JimmCmdSuite.JIMM.Database.AddGroup(context.Background(), group1) c.Assert(err, gc.IsNil) - err = s.jimmSuite.JIMM.Database.AddGroup(context.Background(), group2) + err = s.JimmCmdSuite.JIMM.Database.AddGroup(context.Background(), group2) c.Assert(err, gc.IsNil) totalKeys := 2 for _, tc := range tests { @@ -193,7 +194,7 @@ func (s *relationSuite) TestRemoveRelationSuperuser(c *gc.C) { c.Assert(err, gc.ErrorMatches, tc.message) } else { c.Assert(err, gc.IsNil) - tuples, ct, err := s.jimmSuite.JIMM.OpenFGAClient.ReadRelatedObjects(context.Background(), openfga.Tuple{}, 50, "") + tuples, ct, err := s.JimmCmdSuite.JIMM.OpenFGAClient.ReadRelatedObjects(context.Background(), openfga.Tuple{}, 50, "") c.Assert(err, gc.IsNil) c.Assert(ct, gc.Equals, "") totalKeys-- @@ -203,7 +204,7 @@ func (s *relationSuite) TestRemoveRelationSuperuser(c *gc.C) { } func (s *relationSuite) TestRemoveRelationViaFileSuperuser(c *gc.C) { - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") group1 := "testGroup1" group2 := "testGroup2" group3 := "testGroup3" @@ -228,7 +229,7 @@ func (s *relationSuite) TestRemoveRelationViaFileSuperuser(c *gc.C) { _, err = cmdtesting.RunCommand(c, cmd.NewRemoveRelationCommandForTesting(s.ClientStore(), bClient), "-f", file.Name()) c.Assert(err, gc.IsNil) - tuples, ct, err := s.jimmSuite.JIMM.OpenFGAClient.ReadRelatedObjects(context.Background(), openfga.Tuple{}, 50, "") + tuples, ct, err := s.JimmCmdSuite.JIMM.OpenFGAClient.ReadRelatedObjects(context.Background(), openfga.Tuple{}, 50, "") c.Assert(err, gc.IsNil) c.Assert(ct, gc.Equals, "") c.Logf("existing relations %v", tuples) @@ -249,7 +250,7 @@ func (s *relationSuite) TestRemoveRelationViaFileSuperuser(c *gc.C) { func (s *relationSuite) TestRemoveRelation(c *gc.C) { // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewRemoveRelationCommandForTesting(s.ClientStore(), bClient), "test-group1#member", "member", "test-group2") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } @@ -344,7 +345,7 @@ func initializeEnvironment(c *gc.C, ctx context.Context, db *db.Database, u dbmo func (s *relationSuite) TestListRelations(c *gc.C) { env := initializeEnvironment(c, context.Background(), &s.JIMM.Database, *s.AdminUser) // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") groups := []string{"group-1", "group-2", "group-3"} for _, group := range groups { @@ -437,7 +438,7 @@ user-eve@external administrator applicationoffer-test-controller-1:alice@exte // TODO: remove boilerplate of env setup and use initialiseEnvironment func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { ctx := context.TODO() - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") ofgaClient := s.JIMM.OpenFGAClient // Add some resources to check against @@ -612,7 +613,7 @@ func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { func (s *relationSuite) TestCheckRelation(c *gc.C) { // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand( c, cmd.NewCheckRelationCommandForTesting(s.ClientStore(), bClient), diff --git a/cmd/jimmctl/cmd/removecloudfromcontroller_test.go b/cmd/jimmctl/cmd/removecloudfromcontroller_test.go index 2a7b1b046..f3c693346 100644 --- a/cmd/jimmctl/cmd/removecloudfromcontroller_test.go +++ b/cmd/jimmctl/cmd/removecloudfromcontroller_test.go @@ -9,10 +9,11 @@ import ( apiparams "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" ) type removeCloudFromControllerSuite struct { - jimmSuite + cmdtest.JimmCmdSuite api *fakeRemoveCloudFromControllerAPI } @@ -20,12 +21,12 @@ type removeCloudFromControllerSuite struct { var _ = gc.Suite(&removeCloudFromControllerSuite{}) func (s *removeCloudFromControllerSuite) SetUpTest(c *gc.C) { - s.jimmSuite.SetUpTest(c) + s.JimmCmdSuite.SetUpTest(c) s.api = &fakeRemoveCloudFromControllerAPI{} } func (s *removeCloudFromControllerSuite) TestRemoveCloudFromController(c *gc.C) { - bClient := s.userBakeryClient("alice@external") + bClient := s.UserBakeryClient("alice@external") command := cmd.NewRemoveCloudFromControllerCommandForTesting( s.ClientStore(), @@ -47,7 +48,7 @@ func (s *removeCloudFromControllerSuite) TestRemoveCloudFromController(c *gc.C) } func (s *removeCloudFromControllerSuite) TestRemoveCloudFromControllerWrongArguments(c *gc.C) { - bClient := s.userBakeryClient("alice@external") + bClient := s.UserBakeryClient("alice@external") command := cmd.NewRemoveCloudFromControllerCommandForTesting( s.ClientStore(), @@ -62,7 +63,7 @@ func (s *removeCloudFromControllerSuite) TestRemoveCloudFromControllerWrongArgum } func (s *removeCloudFromControllerSuite) TestRemoveCloudFromControllerCloudNotFound(c *gc.C) { - bClient := s.userBakeryClient("alice@external") + bClient := s.UserBakeryClient("alice@external") command := cmd.NewRemoveCloudFromControllerCommandForTesting( s.ClientStore(), diff --git a/cmd/jimmctl/cmd/removecontroller_test.go b/cmd/jimmctl/cmd/removecontroller_test.go index 22131f0af..5f6f2578f 100644 --- a/cmd/jimmctl/cmd/removecontroller_test.go +++ b/cmd/jimmctl/cmd/removecontroller_test.go @@ -7,11 +7,12 @@ import ( gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/jimmtest" ) type removeControllerSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&removeControllerSuite{}) @@ -20,7 +21,7 @@ func (s *removeControllerSuite) TestRemoveControllerSuperuser(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") context, err := cmdtesting.RunCommand(c, cmd.NewRemoveControllerCommandForTesting(s.ClientStore(), bClient), "controller-1", "--force") c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, `name: controller-1 @@ -69,7 +70,7 @@ func (s *removeControllerSuite) TestRemoveController(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewRemoveControllerCommandForTesting(s.ClientStore(), bClient), "controller-1", "--force") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/revokeauditlogaccess_test.go b/cmd/jimmctl/cmd/revokeauditlogaccess_test.go index 00a4b5485..f4b20f372 100644 --- a/cmd/jimmctl/cmd/revokeauditlogaccess_test.go +++ b/cmd/jimmctl/cmd/revokeauditlogaccess_test.go @@ -7,10 +7,11 @@ import ( gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" ) type revokeAuditLogAccessSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } // TODO (alesstimec) uncomment when grant/revoke is implemented @@ -18,14 +19,14 @@ type revokeAuditLogAccessSuite struct { func (s *revokeAuditLogAccessSuite) TestRevokeAuditLogAccessSuperuser(c *gc.C) { // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") _, err := cmdtesting.RunCommand(c, cmd.NewRevokeAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@external") c.Assert(err, gc.IsNil) } func (s *revokeAuditLogAccessSuite) TestRevokeAuditLogAccess(c *gc.C) { // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewRevokeAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@external") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go b/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go index d5d0c434b..1afba77f7 100644 --- a/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go +++ b/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go @@ -7,11 +7,12 @@ import ( gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/jimmtest" ) type setControllerDeprecatedSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&setControllerDeprecatedSuite{}) @@ -20,7 +21,7 @@ func (s *setControllerDeprecatedSuite) TestSetControllerDeprecatedSuperuser(c *g s.AddController(c, "controller-1", s.APIInfo(c)) // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") context, err := cmdtesting.RunCommand(c, cmd.NewSetControllerDeprecatedCommandForTesting(s.ClientStore(), bClient), "controller-1") c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, `name: controller-1 @@ -69,7 +70,7 @@ func (s *setControllerDeprecatedSuite) TestSetControllerDeprecated(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewSetControllerDeprecatedCommandForTesting(s.ClientStore(), bClient), "controller-1") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/updatemigratedmodel_test.go b/cmd/jimmctl/cmd/updatemigratedmodel_test.go index dd4ba9a30..26e5db7cb 100644 --- a/cmd/jimmctl/cmd/updatemigratedmodel_test.go +++ b/cmd/jimmctl/cmd/updatemigratedmodel_test.go @@ -11,12 +11,13 @@ import ( gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" + "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/jimmtest" ) type updateMigratedModelSuite struct { - jimmSuite + cmdtest.JimmCmdSuite } var _ = gc.Suite(&updateMigratedModelSuite{}) @@ -34,7 +35,7 @@ func (s *updateMigratedModelSuite) TestUpdateMigratedModelSuperuser(c *gc.C) { s.AddController(c, "controller-2", s.APIInfo(c)) // alice is superuser - bClient := s.userBakeryClient("alice") + bClient := s.UserBakeryClient("alice") _, err = cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-2", mt.Id()) c.Assert(err, gc.IsNil) @@ -54,31 +55,31 @@ func (s *updateMigratedModelSuite) TestUpdateMigratedModelUnauthorized(c *gc.C) mt := s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // bob is not superuser - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-1", mt.Id()) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *updateMigratedModelSuite) TestUpdateMigratedModelNoController(c *gc.C) { - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.ErrorMatches, `controller not specified`) } func (s *updateMigratedModelSuite) TestUpdateMigratedModelNoModelUUID(c *gc.C) { - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-id") c.Assert(err, gc.ErrorMatches, `model uuid not specified`) } func (s *updateMigratedModelSuite) TestUpdateMigratedModelInvalidModelUUID(c *gc.C) { - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-id", "not-a-uuid") c.Assert(err, gc.ErrorMatches, `invalid model uuid`) } func (s *updateMigratedModelSuite) TestUpdateMigratedModelTooManyArgs(c *gc.C) { - bClient := s.userBakeryClient("bob") + bClient := s.UserBakeryClient("bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-id", "not-a-uuid", "spare-argument") c.Assert(err, gc.ErrorMatches, `too many args`) } diff --git a/cmd/serviceaccounts/cmd/addserviceaccount.go b/cmd/serviceaccounts/cmd/addserviceaccount.go new file mode 100644 index 000000000..35aace399 --- /dev/null +++ b/cmd/serviceaccounts/cmd/addserviceaccount.go @@ -0,0 +1,100 @@ +// Copyright 2024 Canonical Ltd. + +package cmd + +import ( + "github.com/juju/cmd/v3" + "github.com/juju/gnuflag" + jujuapi "github.com/juju/juju/api" + jujucmd "github.com/juju/juju/cmd" + "github.com/juju/juju/cmd/modelcmd" + "github.com/juju/juju/jujuclient" + + "github.com/canonical/jimm/api" + apiparams "github.com/canonical/jimm/api/params" + "github.com/canonical/jimm/internal/errors" +) + +var ( + addServiceCommandDoc = ` +add command binds a service account to your user, giving you administrator access over the service account. + +Example: + juju service-account add +` +) + +// NewAddControllerCommand returns a command to add a service account +func NewAddServiceAccountCommand() cmd.Command { + cmd := &addServiceAccountCommand{ + store: jujuclient.NewFileClientStore(), + } + + return modelcmd.WrapBase(cmd) +} + +// addServiceAccountCommand binds a service account to a user. +type addServiceAccountCommand struct { + modelcmd.ControllerCommandBase + out cmd.Output + + store jujuclient.ClientStore + dialOpts *jujuapi.DialOpts + clientID string +} + +// Info implements Command.Info. +func (c *addServiceAccountCommand) Info() *cmd.Info { + return jujucmd.Info(&cmd.Info{ + Name: "add", + Purpose: "Add service account", + Doc: addServiceCommandDoc, + }) +} + +// SetFlags implements the cmd.Command interface. +func (c *addServiceAccountCommand) SetFlags(f *gnuflag.FlagSet) { + c.CommandBase.SetFlags(f) + c.out.AddFlags(f, "yaml", map[string]cmd.Formatter{ + "yaml": cmd.FormatYaml, + "json": cmd.FormatJson, + }) +} + +// Init implements the cmd.Command interface. +func (c *addServiceAccountCommand) Init(args []string) error { + if len(args) < 1 { + return errors.E("clientID not specified") + } + c.clientID = args[0] + if len(args) > 1 { + return errors.E("too many args") + } + return nil +} + +// Run implements Command.Run. +func (c *addServiceAccountCommand) Run(ctxt *cmd.Context) error { + currentController, err := c.store.CurrentController() + if err != nil { + return errors.E(err, "could not determine controller") + } + + apiCaller, err := c.NewAPIRootWithDialOpts(c.store, currentController, "", c.dialOpts) + if err != nil { + return err + } + + params := apiparams.AddServiceAccountRequest{ClientID: c.clientID} + client := api.NewClient(apiCaller) + err = client.AddServiceAccount(¶ms) + if err != nil { + return errors.E(err) + } + + err = c.out.Write(ctxt, "service account added successfully") + if err != nil { + return errors.E(err) + } + return nil +} diff --git a/cmd/serviceaccounts/cmd/addserviceaccount_test.go b/cmd/serviceaccounts/cmd/addserviceaccount_test.go new file mode 100644 index 000000000..92ca1abf2 --- /dev/null +++ b/cmd/serviceaccounts/cmd/addserviceaccount_test.go @@ -0,0 +1,47 @@ +// Copyright 2021 Canonical Ltd. + +package cmd_test + +import ( + "context" + + "github.com/juju/cmd/v3/cmdtesting" + "github.com/juju/names/v4" + gc "gopkg.in/check.v1" + + "github.com/canonical/jimm/cmd/serviceaccounts/cmd" + "github.com/canonical/jimm/internal/cmdtest" + "github.com/canonical/jimm/internal/openfga" + ofganames "github.com/canonical/jimm/internal/openfga/names" + jimmnames "github.com/canonical/jimm/pkg/names" +) + +type addServiceAccountSuite struct { + cmdtest.JimmCmdSuite +} + +var _ = gc.Suite(&addServiceAccountSuite{}) + +func (s *addServiceAccountSuite) TestAddServiceAccount(c *gc.C) { + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + // alice is superuser + bClient := s.UserBakeryClient("alice") + _, err := cmdtesting.RunCommand(c, cmd.NewAddServiceAccountCommandForTesting(s.ClientStore(), bClient), clientID) + c.Assert(err, gc.IsNil) + tuple := openfga.Tuple{ + Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), + } + // Check alice has access. + ok, err := s.JIMM.OpenFGAClient.CheckRelation(context.Background(), tuple, false) + c.Assert(err, gc.IsNil) + c.Assert(ok, gc.Equals, true) + // Check that re-running the command doesn't return an error for Alice. + _, err = cmdtesting.RunCommand(c, cmd.NewAddServiceAccountCommandForTesting(s.ClientStore(), bClient), clientID) + c.Assert(err, gc.IsNil) + // Check that re-running the command for a different user returns an error. + bClientBob := s.UserBakeryClient("bob") + _, err = cmdtesting.RunCommand(c, cmd.NewAddServiceAccountCommandForTesting(s.ClientStore(), bClientBob), clientID) + c.Assert(err, gc.ErrorMatches, "service account already owned") +} diff --git a/cmd/serviceaccounts/cmd/export_test.go b/cmd/serviceaccounts/cmd/export_test.go new file mode 100644 index 000000000..fdeb80d90 --- /dev/null +++ b/cmd/serviceaccounts/cmd/export_test.go @@ -0,0 +1,23 @@ +// Copyright 2021 Canonical Ltd. + +package cmd + +import ( + "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" + "github.com/juju/cmd/v3" + jujuapi "github.com/juju/juju/api" + "github.com/juju/juju/cmd/modelcmd" + "github.com/juju/juju/jujuclient" +) + +func NewAddServiceAccountCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { + cmd := &addServiceAccountCommand{ + store: store, + dialOpts: &jujuapi.DialOpts{ + InsecureSkipVerify: true, + BakeryClient: bClient, + }, + } + + return modelcmd.WrapBase(cmd) +} diff --git a/cmd/serviceaccounts/cmd/package_test.go b/cmd/serviceaccounts/cmd/package_test.go new file mode 100644 index 000000000..fb57779d4 --- /dev/null +++ b/cmd/serviceaccounts/cmd/package_test.go @@ -0,0 +1,13 @@ +// Copyright 2021 Canonical Ltd. + +package cmd_test + +import ( + "testing" + + jujutesting "github.com/juju/juju/testing" +) + +func TestPackage(t *testing.T) { + jujutesting.MgoTestPackage(t) +} diff --git a/cmd/service-accounts/main.go b/cmd/serviceaccounts/main.go similarity index 85% rename from cmd/service-accounts/main.go rename to cmd/serviceaccounts/main.go index a57fc45eb..d052142a2 100644 --- a/cmd/service-accounts/main.go +++ b/cmd/serviceaccounts/main.go @@ -6,6 +6,7 @@ import ( "fmt" "os" + "github.com/canonical/jimm/cmd/serviceaccounts/cmd" jujucmd "github.com/juju/cmd/v3" ) @@ -19,7 +20,7 @@ func NewSuperCommand() *jujucmd.SuperCommand { Doc: serviceAccountDoc, }) // Register commands here: - // serviceAccountCmd.Register(cmd.NewCommand()) + serviceAccountCmd.Register(cmd.NewAddServiceAccountCommand()) return serviceAccountCmd } diff --git a/cmd/jimmctl/cmd/jimmsuite_test.go b/internal/cmdtest/jimmsuite.go similarity index 89% rename from cmd/jimmctl/cmd/jimmsuite_test.go rename to internal/cmdtest/jimmsuite.go index 9fc1c5b0a..16baec22f 100644 --- a/cmd/jimmctl/cmd/jimmsuite_test.go +++ b/internal/cmdtest/jimmsuite.go @@ -1,6 +1,8 @@ // Copyright 2021 Canonical Ltd. -package cmd_test +// Package cmdtest provides the test suite used for CLI tests +// as well as helper functions used for integration based CLI tests. +package cmdtest import ( "bytes" @@ -32,7 +34,7 @@ import ( ofganames "github.com/canonical/jimm/internal/openfga/names" ) -type jimmSuite struct { +type JimmCmdSuite struct { jimmtest.CandidSuite corejujutesting.JujuConnSuite @@ -49,7 +51,7 @@ type jimmSuite struct { COFGAParams *cofga.OpenFGAParams } -func (s *jimmSuite) SetUpTest(c *gc.C) { +func (s *JimmCmdSuite) SetUpTest(c *gc.C) { ctx, cancel := context.WithCancel(context.Background()) s.cancel = cancel @@ -140,7 +142,7 @@ func (s *jimmSuite) SetUpTest(c *gc.C) { // commands that use NewAPIRootWithDialOpts. Each invocation of the NewAPIRootWithDialOpts function // updates the ClientStore and removes local IPs thus removing JIMM's IP. // Call this function in your table tests after each test run. -func (s *jimmSuite) RefreshControllerAddress(c *gc.C) { +func (s *JimmCmdSuite) RefreshControllerAddress(c *gc.C) { jimm, ok := s.ClientStore().Controllers["JIMM"] c.Assert(ok, gc.Equals, true) u, err := url.Parse(s.HTTP.URL) @@ -149,7 +151,7 @@ func (s *jimmSuite) RefreshControllerAddress(c *gc.C) { s.ClientStore().Controllers["JIMM"] = jimm } -func (s *jimmSuite) TearDownTest(c *gc.C) { +func (s *JimmCmdSuite) TearDownTest(c *gc.C) { if s.cancel != nil { s.cancel() } @@ -163,7 +165,7 @@ func (s *jimmSuite) TearDownTest(c *gc.C) { s.JujuConnSuite.TearDownTest(c) } -func (s *jimmSuite) userBakeryClient(username string) *httpbakery.Client { +func (s *JimmCmdSuite) UserBakeryClient(username string) *httpbakery.Client { s.Candid.AddUser(username) key := s.Candid.UserPublicKey(username) bClient := httpbakery.NewClient() @@ -181,7 +183,7 @@ func (s *jimmSuite) userBakeryClient(username string) *httpbakery.Client { return bClient } -func (s *jimmSuite) AddController(c *gc.C, name string, info *api.Info) { +func (s *JimmCmdSuite) AddController(c *gc.C, name string, info *api.Info) { ctl := &dbmodel.Controller{ UUID: info.ControllerUUID, Name: name, @@ -205,7 +207,7 @@ func (s *jimmSuite) AddController(c *gc.C, name string, info *api.Info) { c.Assert(err, gc.Equals, nil) } -func (s *jimmSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, cred jujuparams.CloudCredential) { +func (s *JimmCmdSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, cred jujuparams.CloudCredential) { ctx := context.Background() u := dbmodel.Identity{ Name: tag.Owner().Id(), @@ -221,7 +223,7 @@ func (s *jimmSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, c.Assert(err, gc.Equals, nil) } -func (s *jimmSuite) AddModel(c *gc.C, owner names.UserTag, name string, cloud names.CloudTag, region string, cred names.CloudCredentialTag) names.ModelTag { +func (s *JimmCmdSuite) AddModel(c *gc.C, owner names.UserTag, name string, cloud names.CloudTag, region string, cred names.CloudCredentialTag) names.ModelTag { ctx := context.Background() u := openfga.NewUser( &dbmodel.Identity{ diff --git a/internal/jimm/service_account.go b/internal/jimm/service_account.go index 0d431efde..4f3ec4463 100644 --- a/internal/jimm/service_account.go +++ b/internal/jimm/service_account.go @@ -17,6 +17,7 @@ import ( // and then adds a relation between the logged in user and the service account. func (j *JIMM) AddServiceAccount(ctx context.Context, u *openfga.User, clientId string) error { op := errors.Op("jimm.AddServiceAccount") + svcTag := jimmnames.NewServiceAccountTag(clientId) key := openfga.Tuple{ Relation: ofganames.AdministratorRelation, diff --git a/internal/jujuapi/jimm.go b/internal/jujuapi/jimm.go index b6be9d368..b49da379b 100644 --- a/internal/jujuapi/jimm.go +++ b/internal/jujuapi/jimm.go @@ -51,6 +51,8 @@ func init() { migrateModel := rpc.Method(r.MigrateModel) addServiceAccountMethod := rpc.Method(r.AddServiceAccount) updateServiceAccountCredentials := rpc.Method(r.UpdateServiceAccountCredentials) + listServiceAccountCredentials := rpc.Method(r.ListServiceAccountCredentials) + grantServiceAccountAccess := rpc.Method(r.GrantServiceAccountAccess) // JIMM Generic RPC r.AddMethod("JIMM", 4, "AddController", addControllerMethod) @@ -82,6 +84,8 @@ func init() { // JIMM Service Accounts r.AddMethod("JIMM", 4, "AddServiceAccount", addServiceAccountMethod) r.AddMethod("JIMM", 4, "UpdateServiceAccountCredentials", updateServiceAccountCredentials) + r.AddMethod("JIMM", 4, "ListServiceAccountCredentials", listServiceAccountCredentials) + r.AddMethod("JIMM", 4, "GrantServiceAccountAccess", grantServiceAccountAccess) return []int{4} } From 6ccd5a78055e1690cac881e86964672bdffb40bc Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:20:57 +0200 Subject: [PATCH 046/126] CSS-6954 list service account command (#1143) * Add list cloud-credentials for service accounts * Added command and test * Updated wording * Minor tweaks * Added json output test * PR comments --- api/jimm.go | 7 + cmd/serviceaccounts/cmd/export_test.go | 12 ++ .../cmd/listserviceaccountcredentials.go | 204 ++++++++++++++++++ .../cmd/listserviceaccountcredentials_test.go | 111 ++++++++++ cmd/serviceaccounts/main.go | 1 + 5 files changed, 335 insertions(+) create mode 100644 cmd/serviceaccounts/cmd/listserviceaccountcredentials.go create mode 100644 cmd/serviceaccounts/cmd/listserviceaccountcredentials_test.go diff --git a/api/jimm.go b/api/jimm.go index a98573801..3f3d3b72a 100644 --- a/api/jimm.go +++ b/api/jimm.go @@ -195,3 +195,10 @@ func (c *Client) MigrateModel(req *params.MigrateModelRequest) (*jujuparams.Init func (c *Client) AddServiceAccount(req *params.AddServiceAccountRequest) error { return c.caller.APICall("JIMM", 4, "", "AddServiceAccount", req, nil) } + +// ListServiceAccountCredentials lists the cloud credentials belonging to a service account. +func (c *Client) ListServiceAccountCredentials(req *params.ListServiceAccountCredentialsRequest) (*jujuparams.CredentialContentResults, error) { + var response jujuparams.CredentialContentResults + err := c.caller.APICall("JIMM", 4, "", "ListServiceAccountCredentials", req, &response) + return &response, err +} diff --git a/cmd/serviceaccounts/cmd/export_test.go b/cmd/serviceaccounts/cmd/export_test.go index fdeb80d90..8939c162d 100644 --- a/cmd/serviceaccounts/cmd/export_test.go +++ b/cmd/serviceaccounts/cmd/export_test.go @@ -21,3 +21,15 @@ func NewAddServiceAccountCommandForTesting(store jujuclient.ClientStore, bClient return modelcmd.WrapBase(cmd) } + +func NewListServiceAccountCredentialsCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { + cmd := &listServiceAccountCredentialsCommand{ + store: store, + dialOpts: &jujuapi.DialOpts{ + InsecureSkipVerify: true, + BakeryClient: bClient, + }, + } + + return modelcmd.WrapBase(cmd) +} diff --git a/cmd/serviceaccounts/cmd/listserviceaccountcredentials.go b/cmd/serviceaccounts/cmd/listserviceaccountcredentials.go new file mode 100644 index 000000000..284d89722 --- /dev/null +++ b/cmd/serviceaccounts/cmd/listserviceaccountcredentials.go @@ -0,0 +1,204 @@ +// Copyright 2021 Canonical Ltd. + +package cmd + +import ( + "fmt" + "io" + "slices" + "strings" + + "github.com/juju/cmd/v3" + "github.com/juju/gnuflag" + jujuapi "github.com/juju/juju/api" + jujucmd "github.com/juju/juju/cmd" + "github.com/juju/juju/cmd/juju/cloud" + "github.com/juju/juju/cmd/modelcmd" + "github.com/juju/juju/cmd/output" + "github.com/juju/juju/jujuclient" + "github.com/juju/juju/rpc/params" + + "github.com/canonical/jimm/api" + apiparams "github.com/canonical/jimm/api/params" + "github.com/canonical/jimm/internal/errors" +) + +var ( + listServiceCredentialsCommandDoc = ` +list-credentials command list the cloud credentials belonging to a service account. + +This command only shows credentials uploaded to the controller that belong to the service account. + +Client credentials should be managed via juju credentials. + +Example: + juju service-account list-credentials + juju service-account list-credentials --show-secrets + juju service-account list-credentials --format yaml +` +) + +// NewAddControllerCommand returns a command to add a service account +func NewListServiceAccountCredentialsCommand() cmd.Command { + cmd := &listServiceAccountCredentialsCommand{ + store: jujuclient.NewFileClientStore(), + } + + return modelcmd.WrapBase(cmd) +} + +// listServiceAccountCredentialsCommand binds a service account to a user. +type listServiceAccountCredentialsCommand struct { + modelcmd.ControllerCommandBase + out cmd.Output + + store jujuclient.ClientStore + dialOpts *jujuapi.DialOpts + clientID string + showSecrets bool +} + +func (c *listServiceAccountCredentialsCommand) Info() *cmd.Info { + return jujucmd.Info(&cmd.Info{ + Name: "list-credentials", + Purpose: "List service account cloud credentials", + Doc: listServiceCredentialsCommandDoc, + }) +} + +// SetFlags implements Command.SetFlags. +func (c *listServiceAccountCredentialsCommand) SetFlags(f *gnuflag.FlagSet) { + c.CommandBase.SetFlags(f) + c.out.AddFlags(f, "tabular", map[string]cmd.Formatter{ + "yaml": cmd.FormatYaml, + "json": cmd.FormatJson, + "tabular": formatCredentialsTabular, + }) + f.BoolVar(&c.showSecrets, "show-secrets", false, "Show secrets, applicable to yaml or json formats only") +} + +// Init implements the cmd.Command interface. +func (c *listServiceAccountCredentialsCommand) Init(args []string) error { + if len(args) < 1 { + return errors.E("clientID not specified") + } + c.clientID = args[0] + if len(args) > 1 { + return errors.E("too many args") + } + return nil +} + +type credentialsMap struct { + // ServiceAccount has a collection of all ServiceAccount credentials keyed on credential name. + ServiceAccount map[string]cloud.CloudCredential `yaml:"controller-credentials,omitempty" json:"controller-credentials,omitempty"` +} + +// Run implements Command.Run. +func (c *listServiceAccountCredentialsCommand) Run(ctxt *cmd.Context) error { + if c.showSecrets && c.out.Name() == "tabular" { + ctxt.Infof("secrets are not shown in tabular format") + c.showSecrets = false + } + + currentController, err := c.store.CurrentController() + if err != nil { + return errors.E(err, "could not determine controller") + } + + apiCaller, err := c.NewAPIRootWithDialOpts(c.store, currentController, "", c.dialOpts) + if err != nil { + return err + } + + params := apiparams.ListServiceAccountCredentialsRequest{ + ClientID: c.clientID, + CloudCredentialArgs: params.CloudCredentialArgs{IncludeSecrets: c.showSecrets}, + } + client := api.NewClient(apiCaller) + resp, err := client.ListServiceAccountCredentials(¶ms) + if err != nil { + return errors.E(err) + } + svcAccCreds := credentialsMap{ServiceAccount: credentialMapByCloud(*ctxt, resp.Results)} + + err = c.out.Write(ctxt, svcAccCreds) + if err != nil { + return errors.E(err) + } + return nil +} + +func credentialMapByCloud(ctxt cmd.Context, credentials []params.CredentialContentResult) map[string]cloud.CloudCredential { + byCloud := make(map[string]cloud.CloudCredential) + for _, credential := range credentials { + if credential.Error != nil { + ctxt.Warningf("error loading remote credential: %v", credential.Error) + continue + } + remoteCredential := credential.Result.Content + cloudCredential := byCloud[remoteCredential.Cloud] + if cloudCredential.Credentials == nil { + cloudCredential.Credentials = make(map[string]cloud.Credential) + } + cloudCredential.Credentials[remoteCredential.Name] = cloud.Credential{AuthType: remoteCredential.AuthType, Attributes: remoteCredential.Attributes} + byCloud[remoteCredential.Cloud] = cloudCredential + } + return byCloud +} + +// formatCredentialsTabular writes a tabular summary of cloud information. +// Adapted from juju/cmd/juju/cloud/listcredentials.go +func formatCredentialsTabular(writer io.Writer, value interface{}) error { + credentials, ok := value.(credentialsMap) + if !ok { + return errors.E(fmt.Sprintf("expected value of type %T, got %T", credentials, value)) + } + + if len(credentials.ServiceAccount) == 0 { + return nil + } + + tw := output.TabWriter(writer) + w := output.Wrapper{TabWriter: tw} + w.SetColumnAlignRight(1) + + printGroup := func(group map[string]cloud.CloudCredential) { + w.Println("Cloud", "Credentials") + // Sort alphabetically by cloud, and then by credential name. + var cloudNames []string + for name := range group { + cloudNames = append(cloudNames, name) + } + slices.Sort(cloudNames) + + for _, cloudName := range cloudNames { + var haveDefault bool + var credentialNames []string + credentials := group[cloudName] + for credentialName := range credentials.Credentials { + if credentialName == credentials.DefaultCredential { + credentialNames = append([]string{credentialName + "*"}, credentialNames...) + haveDefault = true + } else { + credentialNames = append(credentialNames, credentialName) + } + } + if len(credentialNames) == 0 { + w.Println(fmt.Sprintf("No credentials to display for cloud %v", cloudName)) + continue + } + if haveDefault { + slices.Sort(credentialNames[1:]) + } else { + slices.Sort(credentialNames) + } + w.Println(cloudName, strings.Join(credentialNames, ", ")) + } + } + w.Println("\nController Credentials:") + printGroup(credentials.ServiceAccount) + + tw.Flush() + return nil +} diff --git a/cmd/serviceaccounts/cmd/listserviceaccountcredentials_test.go b/cmd/serviceaccounts/cmd/listserviceaccountcredentials_test.go new file mode 100644 index 000000000..684c0288a --- /dev/null +++ b/cmd/serviceaccounts/cmd/listserviceaccountcredentials_test.go @@ -0,0 +1,111 @@ +// Copyright 2021 Canonical Ltd. + +package cmd_test + +import ( + "context" + "fmt" + + jujucmd "github.com/juju/cmd/v3" + "github.com/juju/cmd/v3/cmdtesting" + "github.com/juju/juju/rpc/params" + "github.com/juju/names/v4" + gc "gopkg.in/check.v1" + + "github.com/canonical/jimm/cmd/serviceaccounts/cmd" + "github.com/canonical/jimm/internal/cmdtest" + "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/jimm" + "github.com/canonical/jimm/internal/openfga" +) + +type listServiceAccountCredentialsSuite struct { + cmdtest.JimmCmdSuite +} + +var _ = gc.Suite(&listServiceAccountCredentialsSuite{}) + +func (s *listServiceAccountCredentialsSuite) TestListServiceAccountCredentials(c *gc.C) { + // Add test cloud for cloud-credential to be valid. + err := s.JIMM.Database.AddCloud(context.Background(), &dbmodel.Cloud{ + Name: "aws", + Regions: []dbmodel.CloudRegion{{Name: "default", CloudName: "test-cloud"}}, + }) + c.Assert(err, gc.IsNil) + // Create Alice Identity and Service Account Identity. + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + // alice is superuser + ctx := context.Background() + user := dbmodel.Identity{Name: "alice@external"} + u := openfga.NewUser(&user, s.OFGAClient) + err = s.JIMM.AddServiceAccount(ctx, u, clientID) + c.Assert(err, gc.IsNil) + svcAcc := dbmodel.Identity{Name: clientID} + err = s.JIMM.Database.GetIdentity(ctx, &svcAcc) + c.Assert(err, gc.IsNil) + svcAccIdentity := openfga.NewUser(&svcAcc, s.OFGAClient) + // Create cloud-credential for service account. + updateArgs := jimm.UpdateCloudCredentialArgs{ + CredentialTag: names.NewCloudCredentialTag(fmt.Sprintf("aws/%s/foo", clientID)), + Credential: params.CloudCredential{Attributes: map[string]string{"foo": "bar"}}, + } + _, err = s.JIMM.UpdateCloudCredential(ctx, svcAccIdentity, updateArgs) + c.Assert(err, gc.IsNil) + + testCases := []struct { + about string + showSecrets bool + expected string + format string + }{ + { + about: "Tabular format output", + showSecrets: false, + expected: ` +Controller Credentials: +Cloud Credentials +aws foo +`, + format: "tabular", + }, + { + about: "Yaml format output with secrets", + showSecrets: true, + expected: `controller-credentials: + aws: + foo: + auth-type: "" + foo: bar +`, + format: "yaml", + }, + { + about: "Yaml format output without secrets", + showSecrets: false, + expected: `controller-credentials: + aws: + foo: + auth-type: "" +`, + format: "yaml", + }, + { + about: "JSON format output with secrets", + showSecrets: true, + expected: `{\"controller-credentials\":{\"aws\":{\"cloud-credentials\":{\"foo\":{\"auth-type\":\"\",\"details\":{\"foo\":\"bar\"}}}}}}\n`, + format: "json", + }, + } + for _, test := range testCases { + c.Log(test.about) + bClient := s.UserBakeryClient("alice") + var result *jujucmd.Context + if test.showSecrets { + result, err = cmdtesting.RunCommand(c, cmd.NewListServiceAccountCredentialsCommandForTesting(s.ClientStore(), bClient), clientID, "--format", test.format, "--show-secrets") + } else { + result, err = cmdtesting.RunCommand(c, cmd.NewListServiceAccountCredentialsCommandForTesting(s.ClientStore(), bClient), clientID, "--format", test.format) + } + c.Assert(err, gc.IsNil) + c.Assert(cmdtesting.Stdout(result), gc.Matches, test.expected) + } +} diff --git a/cmd/serviceaccounts/main.go b/cmd/serviceaccounts/main.go index d052142a2..d16645741 100644 --- a/cmd/serviceaccounts/main.go +++ b/cmd/serviceaccounts/main.go @@ -21,6 +21,7 @@ func NewSuperCommand() *jujucmd.SuperCommand { }) // Register commands here: serviceAccountCmd.Register(cmd.NewAddServiceAccountCommand()) + serviceAccountCmd.Register(cmd.NewListServiceAccountCredentialsCommand()) return serviceAccountCmd } From d153bd9aa2b35f34e3ee89150a1348ceedb2b9d8 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:16:58 +0000 Subject: [PATCH 047/126] feat(oauth2.go): introduce JWT creation and validation for CLI sessions (#1142) * feat(oauth2.go): introduce JWT creation and validation for CLI sessions * feat(package doc for auth): adds a package doc for auth * feat(auth): update error code * PR comments --- docker-compose.yaml | 1 + internal/auth/auth.go | 10 ++++ internal/auth/oauth2.go | 52 +++++++++++++++++++- internal/auth/oauth2_test.go | 94 ++++++++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 internal/auth/auth.go diff --git a/docker-compose.yaml b/docker-compose.yaml index e231af986..64e195a24 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -227,6 +227,7 @@ services: keycloak: image: docker.io/bitnami/keycloak:23 + container_name: keycloak environment: KEYCLOAK_HTTP_PORT: 8082 KEYCLOAK_ENABLE_HEALTH_ENDPOINTS: true diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 000000000..6aa1b3ebd --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,10 @@ +// Copyright 2024 canonical. + +// Package auth provides means to authenticate users into JIMM. +// +// The methods of authentication are: +// - Macaroons (deprecated) +// - OAuth2.0 (Device flow) +// - OAuth2.0 (Browser flow) +// - JWTs (For CLI based sessions) +package auth diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index d11883f1a..2a808f5f7 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -4,8 +4,12 @@ package auth import ( "context" + "net/mail" + "time" "github.com/coreos/go-oidc/v3/oidc" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwt" "golang.org/x/oauth2" "github.com/canonical/jimm/internal/errors" @@ -17,6 +21,8 @@ type AuthenticationService struct { // provider holds a OIDC provider wrapper for the OAuth2.0 /x/oauth package, // enabling UserInfo calls, wellknown retrieval and jwks verification. provider *oidc.Provider + // accessTokenExpiry holds the expiry time for JIMM minted access tokens (JWTs). + accessTokenExpiry time.Duration } // AuthenticationServiceParams holds the parameters to initialise @@ -31,6 +37,8 @@ type AuthenticationServiceParams struct { DeviceClientID string // DeviceScopes holds the scopes that you wish to retrieve. DeviceScopes []string + // AccessTokenExpiry holds the expiry time of minted JIMM access tokens (JWTs). + AccessTokenExpiry time.Duration } // NewAuthenticationService returns a new authentication service for handling @@ -40,7 +48,7 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP provider, err := oidc.NewProvider(ctx, params.IssuerURL) if err != nil { - return nil, errors.E(op, err, "failed to create oidc provider") + return nil, errors.E(op, errors.CodeServerConfiguration, err, "failed to create oidc provider") } return &AuthenticationService{ @@ -50,6 +58,7 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP Endpoint: provider.Endpoint(), Scopes: params.DeviceScopes, }, + accessTokenExpiry: params.AccessTokenExpiry, }, nil } @@ -81,7 +90,7 @@ func (as *AuthenticationService) Device(ctx context.Context) (*oauth2.DeviceAuth // DeviceAccessToken continues and collect an access token during the device login flow // and is step TWO. // -// See Device(...) godoc for more info pertaining to the fko. +// See Device(...) godoc for more info pertaining to the flow. func (as *AuthenticationService) DeviceAccessToken(ctx context.Context, res *oauth2.DeviceAuthResponse) (*oauth2.Token, error) { const op = errors.Op("auth.AuthenticationService.DeviceAccessToken") @@ -133,5 +142,44 @@ func (as *AuthenticationService) Email(idToken *oidc.IDToken) (string, error) { } return claims.Email, nil +} + +// MintAccessToken mints a session access token to be used when logging into JIMM +// via an access token. The token only contains the user's email for authentication. +func (as *AuthenticationService) MintAccessToken(email string, secretKey string) ([]byte, error) { + const op = errors.Op("auth.AuthenticationService.MintAccessToken") + + token, err := jwt.NewBuilder(). + Subject(email). + Expiration(time.Now().Add(as.accessTokenExpiry)). + Build() + if err != nil { + return nil, errors.E(op, err, "failed to build access token") + } + + freshToken, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte(secretKey))) + if err != nil { + return nil, errors.E(op, err, "failed to sign access token") + } + return freshToken, nil +} + +// VerifyAccessToken symmetrically verifies the validty of the signature on the +// access token JWT, returning the parsed token. +// +// The subject of the token contains the user's email and can be used +// for user object creation. +func (as *AuthenticationService) VerifyAccessToken(token []byte, secretKey string) (jwt.Token, error) { + const op = errors.Op("auth.AuthenticationService.VerifyAccessToken") + + parsedToken, err := jwt.Parse(token, jwt.WithKey(jwa.HS256, []byte(secretKey))) + if err != nil { + return nil, errors.E(op, err) + } + + if _, err = mail.ParseAddress(parsedToken.Subject()); err != nil { + return nil, errors.E(op, "failed to parse email") + } + return parsedToken, nil } diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 82a2d6866..21fb7f1ea 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -11,6 +11,7 @@ import ( "net/url" "regexp" "testing" + "time" "github.com/canonical/jimm/internal/auth" "github.com/coreos/go-oidc/v3/oidc" @@ -109,3 +110,96 @@ func TestDevice(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(email, qt.Equals, "jimm-test@canonical.com") } + +// TestAccessTokens tests both the minting and validation of JIMM +// access tokens. +func TestAccessTokens(t *testing.T) { + c := qt.New(t) + + ctx := context.Background() + + authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + AccessTokenExpiry: time.Hour, + }) + c.Assert(err, qt.IsNil) + + secretKey := "secret-key" + token, err := authSvc.MintAccessToken("jimm-test@canonical.com", secretKey) + c.Assert(err, qt.IsNil) + c.Assert(len(token) > 0, qt.IsTrue) + + jwtToken, err := authSvc.VerifyAccessToken(token, secretKey) + c.Assert(err, qt.IsNil) + c.Assert(jwtToken.Subject(), qt.Equals, "jimm-test@canonical.com") +} + +func TestAccessTokenRejectsWrongSecretKey(t *testing.T) { + c := qt.New(t) + + ctx := context.Background() + + authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + AccessTokenExpiry: time.Hour, + }) + c.Assert(err, qt.IsNil) + + secretKey := "secret-key" + token, err := authSvc.MintAccessToken("jimm-test@canonical.com", secretKey) + c.Assert(err, qt.IsNil) + c.Assert(len(token) > 0, qt.IsTrue) + + _, err = authSvc.VerifyAccessToken(token, "wrong key") + c.Assert(err, qt.ErrorMatches, "could not verify message using any of the signatures or keys") +} + +func TestAccessTokenRejectsExpiredToken(t *testing.T) { + c := qt.New(t) + + ctx := context.Background() + + noDuration := time.Duration(0) + + authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + AccessTokenExpiry: noDuration, + }) + c.Assert(err, qt.IsNil) + + secretKey := "secret-key" + token, err := authSvc.MintAccessToken("jimm-test@canonical.com", secretKey) + c.Assert(err, qt.IsNil) + c.Assert(len(token) > 0, qt.IsTrue) + + _, err = authSvc.VerifyAccessToken(token, secretKey) + c.Assert(err, qt.ErrorMatches, `"exp" not satisfied`) +} + +func TestAccessTokenValidatesEmail(t *testing.T) { + c := qt.New(t) + + ctx := context.Background() + + authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + AccessTokenExpiry: time.Hour, + }) + c.Assert(err, qt.IsNil) + + secretKey := "secret-key" + token, err := authSvc.MintAccessToken("", secretKey) + c.Assert(err, qt.IsNil) + c.Assert(len(token) > 0, qt.IsTrue) + + _, err = authSvc.VerifyAccessToken(token, secretKey) + c.Assert(err, qt.ErrorMatches, "failed to parse email") +} From 1a97ef7352f4f045b881d6fe010269d5d25950ae Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 1 Feb 2024 14:52:41 +0000 Subject: [PATCH 048/126] CSS-6955 Add `grant` command to `service-accounts` CLI (#1146) * Add `grant` command to `service-accounts` CLI Signed-off-by: Babak K. Shandiz * Improve `grant` command description Signed-off-by: Babak K. Shandiz * Organize `grant` command docs using `jujucmd` standard fields Signed-off-by: Babak K. Shandiz * Improve error message for controller dial failure Signed-off-by: Babak K. Shandiz * Add missing godoc Signed-off-by: Babak K. Shandiz * Add more tests Signed-off-by: Babak K. Shandiz * Remove YAML/JSON formatters Signed-off-by: Babak K. Shandiz * Remove unrelated tests Signed-off-by: Babak K. Shandiz * Group individual tests into a table test Signed-off-by: Babak K. Shandiz * Fix printing `access granted` on success Signed-off-by: Babak K. Shandiz --------- Signed-off-by: Babak K. Shandiz --- api/jimm.go | 5 ++ cmd/serviceaccounts/cmd/export_test.go | 12 ++++ cmd/serviceaccounts/cmd/grant.go | 97 ++++++++++++++++++++++++++ cmd/serviceaccounts/cmd/grant_test.go | 94 +++++++++++++++++++++++++ cmd/serviceaccounts/main.go | 1 + 5 files changed, 209 insertions(+) create mode 100644 cmd/serviceaccounts/cmd/grant.go create mode 100644 cmd/serviceaccounts/cmd/grant_test.go diff --git a/api/jimm.go b/api/jimm.go index 3f3d3b72a..d89ebf63d 100644 --- a/api/jimm.go +++ b/api/jimm.go @@ -202,3 +202,8 @@ func (c *Client) ListServiceAccountCredentials(req *params.ListServiceAccountCre err := c.caller.APICall("JIMM", 4, "", "ListServiceAccountCredentials", req, &response) return &response, err } + +// GrantServiceAccountAccess grants admin access to a service account to given groups/identities. +func (c *Client) GrantServiceAccountAccess(req *params.GrantServiceAccountAccess) error { + return c.caller.APICall("JIMM", 4, "", "GrantServiceAccountAccess", req, nil) +} diff --git a/cmd/serviceaccounts/cmd/export_test.go b/cmd/serviceaccounts/cmd/export_test.go index 8939c162d..ac8099372 100644 --- a/cmd/serviceaccounts/cmd/export_test.go +++ b/cmd/serviceaccounts/cmd/export_test.go @@ -33,3 +33,15 @@ func NewListServiceAccountCredentialsCommandForTesting(store jujuclient.ClientSt return modelcmd.WrapBase(cmd) } + +func NewGrantCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { + cmd := &grantCommand{ + store: store, + dialOpts: &jujuapi.DialOpts{ + InsecureSkipVerify: true, + BakeryClient: bClient, + }, + } + + return modelcmd.WrapBase(cmd) +} diff --git a/cmd/serviceaccounts/cmd/grant.go b/cmd/serviceaccounts/cmd/grant.go new file mode 100644 index 000000000..c93d788c3 --- /dev/null +++ b/cmd/serviceaccounts/cmd/grant.go @@ -0,0 +1,97 @@ +// Copyright 2024 Canonical Ltd. + +package cmd + +import ( + "fmt" + + "github.com/juju/cmd/v3" + jujuapi "github.com/juju/juju/api" + jujucmd "github.com/juju/juju/cmd" + "github.com/juju/juju/cmd/modelcmd" + "github.com/juju/juju/jujuclient" + + "github.com/canonical/jimm/api" + apiparams "github.com/canonical/jimm/api/params" + "github.com/canonical/jimm/internal/errors" +) + +var ( + grantCommandDoc = ` +grant command grants administrator access over a service account to the given groups/identities. +` + grantCommandExamples = ` + juju service-accounts grant 00000000-0000-0000-0000-000000000000 user-foo group-bar +` +) + +// NewGrantCommand returns a command to grant admin access to a service account to given groups/identities. +func NewGrantCommand() cmd.Command { + cmd := &grantCommand{ + store: jujuclient.NewFileClientStore(), + } + + return modelcmd.WrapBase(cmd) +} + +// grantCommand grants admin access to a service account to given groups/identities. +type grantCommand struct { + modelcmd.ControllerCommandBase + out cmd.Output + + store jujuclient.ClientStore + dialOpts *jujuapi.DialOpts + + clientID string + entities []string +} + +// Info implements Command.Info. +func (c *grantCommand) Info() *cmd.Info { + return jujucmd.Info(&cmd.Info{ + Name: "grant", + Args: " (|) [(|) ...]", + Purpose: "Grants administrator access over a service account to the given groups/identities", + Examples: grantCommandExamples, + Doc: grantCommandDoc, + }) +} + +// Init implements the cmd.Command interface. +func (c *grantCommand) Init(args []string) error { + if len(args) < 1 { + return errors.E("client ID not specified") + } + c.clientID = args[0] + if len(args) < 2 { + return errors.E("user/group not specified") + } + c.entities = args[1:] + return nil +} + +// Run implements Command.Run. +func (c *grantCommand) Run(ctxt *cmd.Context) error { + currentController, err := c.store.CurrentController() + if err != nil { + return errors.E(err, "could not determine controller") + } + + apiCaller, err := c.NewAPIRootWithDialOpts(c.store, currentController, "", c.dialOpts) + if err != nil { + return errors.E(err, "failed to dial the controller") + } + + params := apiparams.GrantServiceAccountAccess{ + ClientID: c.clientID, + Entities: c.entities, + } + + client := api.NewClient(apiCaller) + err = client.GrantServiceAccountAccess(¶ms) + if err != nil { + return errors.E(err) + } + fmt.Fprintln(ctxt.Stdout, "access granted") + return nil +} diff --git a/cmd/serviceaccounts/cmd/grant_test.go b/cmd/serviceaccounts/cmd/grant_test.go new file mode 100644 index 000000000..66a851392 --- /dev/null +++ b/cmd/serviceaccounts/cmd/grant_test.go @@ -0,0 +1,94 @@ +// Copyright 2024 Canonical Ltd. + +package cmd_test + +import ( + "context" + + "github.com/juju/cmd/v3/cmdtesting" + "github.com/juju/names/v4" + gc "gopkg.in/check.v1" + + "github.com/canonical/jimm/cmd/serviceaccounts/cmd" + "github.com/canonical/jimm/internal/cmdtest" + "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/openfga" + ofganames "github.com/canonical/jimm/internal/openfga/names" + jimmnames "github.com/canonical/jimm/pkg/names" +) + +type grantSuite struct { + cmdtest.JimmCmdSuite +} + +var _ = gc.Suite(&grantSuite{}) + +func (s *grantSuite) TestGrant(c *gc.C) { + ctx := context.Background() + + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + + // alice is superuser + bClient := s.UserBakeryClient("alice") + + sa := dbmodel.Identity{ + Name: clientID, + } + err := s.JIMM.Database.GetIdentity(ctx, &sa) + c.Assert(err, gc.IsNil) + + // Make alice admin of the service account + tuple := openfga.Tuple{ + Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), + } + err = s.JIMM.OpenFGAClient.AddRelation(ctx, tuple) + c.Assert(err, gc.IsNil) + + err = s.JIMM.Database.AddGroup(ctx, "1") + c.Assert(err, gc.IsNil) + + cmdContext, err := cmdtesting.RunCommand(c, cmd.NewGrantCommandForTesting(s.ClientStore(), bClient), clientID, "user-bob", "group-1") + c.Assert(err, gc.IsNil) + c.Assert(cmdtesting.Stdout(cmdContext), gc.Equals, "access granted\n") + + ok, err := s.JIMM.OpenFGAClient.CheckRelation(ctx, openfga.Tuple{ + Object: ofganames.ConvertTag(names.NewUserTag("bob")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), + }, false) + c.Assert(err, gc.IsNil) + c.Assert(ok, gc.Equals, true) + + ok, err = s.JIMM.OpenFGAClient.CheckRelation(ctx, openfga.Tuple{ + Object: ofganames.ConvertTag(jimmnames.NewGroupTag("1#member")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), + }, false) + c.Assert(err, gc.IsNil) + c.Assert(ok, gc.Equals, true) +} + +func (s *grantSuite) TestMissingArgs(c *gc.C) { + tests := []struct { + name string + args []string + expectedError string + }{{ + name: "missing client ID", + args: []string{}, + expectedError: "client ID not specified", + }, { + name: "missing identity (user/group)", + args: []string{"some-client-id"}, + expectedError: "user/group not specified", + }} + + bClient := s.UserBakeryClient("alice") + clientStore := s.ClientStore() + for _, t := range tests { + _, err := cmdtesting.RunCommand(c, cmd.NewGrantCommandForTesting(clientStore, bClient), t.args...) + c.Assert(err, gc.ErrorMatches, t.expectedError, gc.Commentf("test case failed: %q", t.name)) + } +} diff --git a/cmd/serviceaccounts/main.go b/cmd/serviceaccounts/main.go index d16645741..ddb3983cd 100644 --- a/cmd/serviceaccounts/main.go +++ b/cmd/serviceaccounts/main.go @@ -22,6 +22,7 @@ func NewSuperCommand() *jujucmd.SuperCommand { // Register commands here: serviceAccountCmd.Register(cmd.NewAddServiceAccountCommand()) serviceAccountCmd.Register(cmd.NewListServiceAccountCredentialsCommand()) + serviceAccountCmd.Register(cmd.NewGrantCommand()) return serviceAccountCmd } From cbc93af17471c30f07f9b84869188675238bcb44 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 1 Feb 2024 22:11:38 +0000 Subject: [PATCH 049/126] CSS-6953 Add `update-credentials` command to `service-accounts` CLI (#1145) * Add `service-accounts update-credentials` command Signed-off-by: Babak K. Shandiz * Fix godoc Signed-off-by: Babak K. Shandiz * Fix copyrights Signed-off-by: Babak K. Shandiz * Register `update-credentials` command Signed-off-by: Babak K. Shandiz * Add missing godoc Signed-off-by: Babak K. Shandiz * Improve error message for controller dial failure Signed-off-by: Babak K. Shandiz * Wrap returned error with `errors.E` Signed-off-by: Babak K. Shandiz * Improve error message when credential was not found Signed-off-by: Babak K. Shandiz * Explain the command adds non-existing credentials Signed-off-by: Babak K. Shandiz * Organize command docs using `jujucmd` standard fields Signed-off-by: Babak K. Shandiz * Add test to verify JIMM upserts new credentials Signed-off-by: Babak K. Shandiz * Fix unformatted error messages Signed-off-by: Babak K. Shandiz * Add more test cases Signed-off-by: Babak K. Shandiz * Remove unrelated tests Signed-off-by: Babak K. Shandiz * Group individual tests into a table test Signed-off-by: Babak K. Shandiz --------- Signed-off-by: Babak K. Shandiz --- api/jimm.go | 7 + .../cmd/addserviceaccount_test.go | 2 +- cmd/serviceaccounts/cmd/export_test.go | 14 +- cmd/serviceaccounts/cmd/package_test.go | 2 +- cmd/serviceaccounts/cmd/updatecredentials.go | 154 +++++++++++++ .../cmd/updatecredentials_test.go | 216 ++++++++++++++++++ cmd/serviceaccounts/main.go | 1 + 7 files changed, 393 insertions(+), 3 deletions(-) create mode 100644 cmd/serviceaccounts/cmd/updatecredentials.go create mode 100644 cmd/serviceaccounts/cmd/updatecredentials_test.go diff --git a/api/jimm.go b/api/jimm.go index d89ebf63d..86a238761 100644 --- a/api/jimm.go +++ b/api/jimm.go @@ -203,6 +203,13 @@ func (c *Client) ListServiceAccountCredentials(req *params.ListServiceAccountCre return &response, err } +// UpdateServiceAccountCredentials updates credentials associated with a service account. +func (c *Client) UpdateServiceAccountCredentials(req *params.UpdateServiceAccountCredentialsRequest) (*jujuparams.UpdateCredentialResults, error) { + var response jujuparams.UpdateCredentialResults + err := c.caller.APICall("JIMM", 4, "", "UpdateServiceAccountCredentials", req, &response) + return &response, err +} + // GrantServiceAccountAccess grants admin access to a service account to given groups/identities. func (c *Client) GrantServiceAccountAccess(req *params.GrantServiceAccountAccess) error { return c.caller.APICall("JIMM", 4, "", "GrantServiceAccountAccess", req, nil) diff --git a/cmd/serviceaccounts/cmd/addserviceaccount_test.go b/cmd/serviceaccounts/cmd/addserviceaccount_test.go index 92ca1abf2..431b1a96a 100644 --- a/cmd/serviceaccounts/cmd/addserviceaccount_test.go +++ b/cmd/serviceaccounts/cmd/addserviceaccount_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical Ltd. package cmd_test diff --git a/cmd/serviceaccounts/cmd/export_test.go b/cmd/serviceaccounts/cmd/export_test.go index ac8099372..7e8121749 100644 --- a/cmd/serviceaccounts/cmd/export_test.go +++ b/cmd/serviceaccounts/cmd/export_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical Ltd. package cmd @@ -34,6 +34,18 @@ func NewListServiceAccountCredentialsCommandForTesting(store jujuclient.ClientSt return modelcmd.WrapBase(cmd) } +func NewUpdateCredentialsCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { + cmd := &updateCredentialsCommand{ + store: store, + dialOpts: &jujuapi.DialOpts{ + InsecureSkipVerify: true, + BakeryClient: bClient, + }, + } + + return modelcmd.WrapBase(cmd) +} + func NewGrantCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { cmd := &grantCommand{ store: store, diff --git a/cmd/serviceaccounts/cmd/package_test.go b/cmd/serviceaccounts/cmd/package_test.go index fb57779d4..1a5f86c31 100644 --- a/cmd/serviceaccounts/cmd/package_test.go +++ b/cmd/serviceaccounts/cmd/package_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical Ltd. package cmd_test diff --git a/cmd/serviceaccounts/cmd/updatecredentials.go b/cmd/serviceaccounts/cmd/updatecredentials.go new file mode 100644 index 000000000..c979a9b85 --- /dev/null +++ b/cmd/serviceaccounts/cmd/updatecredentials.go @@ -0,0 +1,154 @@ +// Copyright 2024 Canonical Ltd. + +package cmd + +import ( + "fmt" + + "github.com/juju/cmd/v3" + "github.com/juju/gnuflag" + jujuapi "github.com/juju/juju/api" + jujucmd "github.com/juju/juju/cmd" + "github.com/juju/juju/cmd/modelcmd" + "github.com/juju/juju/jujuclient" + "github.com/juju/names/v4" + + jujuparams "github.com/juju/juju/rpc/params" + + "github.com/canonical/jimm/api" + apiparams "github.com/canonical/jimm/api/params" + "github.com/canonical/jimm/internal/errors" +) + +var ( + updateCredentialsCommandDoc = ` +update-credentials command updates the credentials associated with a service account. +This will add the credentials to JAAS if they were not found. +` + + updateCredentialsCommandExamples = ` + juju service-account update-credentials 00000000-0000-0000-0000-000000000000 aws credential-name +` +) + +// NewUpdateCredentialsCommand returns a command to update a service account's cloud credentials. +func NewUpdateCredentialsCommand() cmd.Command { + cmd := &updateCredentialsCommand{ + store: jujuclient.NewFileClientStore(), + } + + return modelcmd.WrapBase(cmd) +} + +// updateCredentialsCommand updates a service account's cloud credentials. +type updateCredentialsCommand struct { + modelcmd.ControllerCommandBase + out cmd.Output + + store jujuclient.ClientStore + dialOpts *jujuapi.DialOpts + + clientID string + cloud string + credentialName string +} + +// Info implements Command.Info. +func (c *updateCredentialsCommand) Info() *cmd.Info { + return jujucmd.Info(&cmd.Info{ + Name: "update-credentials", + Purpose: "Update service account cloud credentials", + Args: " ", + Doc: updateCredentialsCommandDoc, + Examples: updateCredentialsCommandExamples, + }) +} + +// SetFlags implements Command.SetFlags. +func (c *updateCredentialsCommand) SetFlags(f *gnuflag.FlagSet) { + c.CommandBase.SetFlags(f) + c.out.AddFlags(f, "yaml", map[string]cmd.Formatter{ + "yaml": cmd.FormatYaml, + "json": cmd.FormatJson, + }) +} + +// Init implements the cmd.Command interface. +func (c *updateCredentialsCommand) Init(args []string) error { + if len(args) < 1 { + return errors.E("client ID not specified") + } + c.clientID = args[0] + if len(args) < 2 { + return errors.E("cloud not specified") + } + c.cloud = args[1] + if len(args) < 3 { + return errors.E("credential name not specified") + } + c.credentialName = args[2] + if len(args) > 3 { + return errors.E("too many args") + } + return nil +} + +// Run implements Command.Run. +func (c *updateCredentialsCommand) Run(ctxt *cmd.Context) error { + currentController, err := c.store.CurrentController() + if err != nil { + return errors.E(err, "could not determine controller") + } + + apiCaller, err := c.NewAPIRootWithDialOpts(c.store, currentController, "", c.dialOpts) + if err != nil { + return errors.E(err, "failed to dial the controller") + } + + credential, err := findCredentialsInLocalCache(c.store, c.cloud, c.credentialName) + if err != nil { + return errors.E(err) + } + + taggedCredential := jujuparams.TaggedCredential{ + Tag: names.NewCloudCredentialTag(fmt.Sprintf("%s/%s/%s", c.cloud, c.clientID, c.credentialName)).String(), + Credential: *credential, + } + + params := apiparams.UpdateServiceAccountCredentialsRequest{ + ClientID: c.clientID, + UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ + Credentials: []jujuparams.TaggedCredential{taggedCredential}, + }, + } + + client := api.NewClient(apiCaller) + resp, err := client.UpdateServiceAccountCredentials(¶ms) + if err != nil { + return errors.E(err) + } + + err = c.out.Write(ctxt, resp) + if err != nil { + return errors.E(err) + } + return nil +} + +func findCredentialsInLocalCache(store jujuclient.ClientStore, cloud, credentialName string) (*jujuparams.CloudCredential, error) { + cloudCredentials, err := store.CredentialForCloud(cloud) + if err != nil { + return nil, errors.E(err, fmt.Sprintf("failed to fetch local credentials for cloud %q", cloud)) + } + + for name, aCredential := range cloudCredentials.AuthCredentials { + if name == credentialName { + return &jujuparams.CloudCredential{ + AuthType: string(aCredential.AuthType()), + Attributes: aCredential.Attributes(), + }, nil + } + } + + return nil, errors.E(fmt.Sprintf("credential %q not found on local client; run `juju add-credential --client` to add the credential to Juju local store first", credentialName)) +} diff --git a/cmd/serviceaccounts/cmd/updatecredentials_test.go b/cmd/serviceaccounts/cmd/updatecredentials_test.go new file mode 100644 index 000000000..8f69684fe --- /dev/null +++ b/cmd/serviceaccounts/cmd/updatecredentials_test.go @@ -0,0 +1,216 @@ +// Copyright 2024 Canonical Ltd. + +package cmd_test + +import ( + "context" + + "github.com/juju/cmd/v3/cmdtesting" + "github.com/juju/names/v4" + gc "gopkg.in/check.v1" + + "github.com/canonical/jimm/cmd/serviceaccounts/cmd" + "github.com/canonical/jimm/internal/cmdtest" + "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/openfga" + ofganames "github.com/canonical/jimm/internal/openfga/names" + jimmnames "github.com/canonical/jimm/pkg/names" + jujucloud "github.com/juju/juju/cloud" +) + +type updateCredentialsSuite struct { + cmdtest.JimmCmdSuite +} + +var _ = gc.Suite(&updateCredentialsSuite{}) + +func (s *updateCredentialsSuite) TestUpdateCredentialsWithNewCredentials(c *gc.C) { + ctx := context.Background() + + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + + // alice is superuser + bClient := s.UserBakeryClient("alice") + + sa := dbmodel.Identity{ + Name: clientID, + } + err := s.JIMM.Database.GetIdentity(ctx, &sa) + c.Assert(err, gc.IsNil) + + // Make alice admin of the service account + tuple := openfga.Tuple{ + Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), + } + err = s.JIMM.OpenFGAClient.AddRelation(ctx, tuple) + c.Assert(err, gc.IsNil) + + cloud := dbmodel.Cloud{ + Name: "test-cloud", + Type: "kubernetes", + } + err = s.JIMM.Database.AddCloud(ctx, &cloud) + c.Assert(err, gc.IsNil) + + clientStore := s.ClientStore() + + err = clientStore.UpdateCredential("test-cloud", jujucloud.CloudCredential{ + AuthCredentials: map[string]jujucloud.Credential{ + "test-credentials": jujucloud.NewCredential(jujucloud.EmptyAuthType, map[string]string{ + "foo": "bar", + }), + }, + }) + c.Assert(err, gc.IsNil) + + cmdContext, err := cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(clientStore, bClient), clientID, "test-cloud", "test-credentials") + c.Assert(err, gc.IsNil) + c.Assert(cmdtesting.Stdout(cmdContext), gc.Equals, `results: +- credentialtag: cloudcred-test-cloud_abda51b2-d735-4794-a8bd-49c506baa4af_test-credentials + error: null + models: [] +`) + + ofgaUser := openfga.NewUser(&sa, s.JIMM.AuthorizationClient()) + cloudCredentialTag := names.NewCloudCredentialTag("test-cloud/" + clientID + "/test-credentials") + cloudCredential2, err := s.JIMM.GetCloudCredential(ctx, ofgaUser, cloudCredentialTag) + c.Assert(err, gc.IsNil) + attrs, _, err := s.JIMM.GetCloudCredentialAttributes(ctx, ofgaUser, cloudCredential2, true) + c.Assert(err, gc.IsNil) + + c.Assert(attrs, gc.DeepEquals, map[string]string{ + "foo": "bar", + }) +} + +func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c *gc.C) { + ctx := context.Background() + + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + + // alice is superuser + bClient := s.UserBakeryClient("alice") + + sa := dbmodel.Identity{ + Name: clientID, + } + err := s.JIMM.Database.GetIdentity(ctx, &sa) + c.Assert(err, gc.IsNil) + + // Make alice admin of the service account + tuple := openfga.Tuple{ + Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), + } + err = s.JIMM.OpenFGAClient.AddRelation(ctx, tuple) + c.Assert(err, gc.IsNil) + + cloud := dbmodel.Cloud{ + Name: "test-cloud", + Type: "kubernetes", + } + err = s.JIMM.Database.AddCloud(ctx, &cloud) + c.Assert(err, gc.IsNil) + + cloudCredential := dbmodel.CloudCredential{ + Name: "test-credentials", + CloudName: "test-cloud", + OwnerIdentityName: clientID, + AuthType: "empty", + } + err = s.JIMM.Database.SetCloudCredential(ctx, &cloudCredential) + c.Assert(err, gc.IsNil) + + clientStore := s.ClientStore() + + err = clientStore.UpdateCredential("test-cloud", jujucloud.CloudCredential{ + AuthCredentials: map[string]jujucloud.Credential{ + "test-credentials": jujucloud.NewCredential(jujucloud.EmptyAuthType, map[string]string{ + "foo": "bar", + }), + }, + }) + c.Assert(err, gc.IsNil) + + cmdContext, err := cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(clientStore, bClient), clientID, "test-cloud", "test-credentials") + c.Assert(err, gc.IsNil) + c.Assert(cmdtesting.Stdout(cmdContext), gc.Equals, `results: +- credentialtag: cloudcred-test-cloud_abda51b2-d735-4794-a8bd-49c506baa4af_test-credentials + error: null + models: [] +`) + + ofgaUser := openfga.NewUser(&sa, s.JIMM.AuthorizationClient()) + cloudCredentialTag := names.NewCloudCredentialTag("test-cloud/" + clientID + "/test-credentials") + cloudCredential2, err := s.JIMM.GetCloudCredential(ctx, ofgaUser, cloudCredentialTag) + c.Assert(err, gc.IsNil) + attrs, _, err := s.JIMM.GetCloudCredentialAttributes(ctx, ofgaUser, cloudCredential2, true) + c.Assert(err, gc.IsNil) + + c.Assert(attrs, gc.DeepEquals, map[string]string{ + "foo": "bar", + }) +} + +func (s *updateCredentialsSuite) TestCloudNotInLocalStore(c *gc.C) { + bClient := s.UserBakeryClient("alice") + _, err := cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(s.ClientStore(), bClient), + "00000000-0000-0000-0000-000000000000", + "non-existing-cloud", + "foo", + ) + c.Assert(err, gc.ErrorMatches, "failed to fetch local credentials for cloud \"non-existing-cloud\"") +} + +func (s *updateCredentialsSuite) TestCredentialNotInLocalStore(c *gc.C) { + bClient := s.UserBakeryClient("alice") + + clientStore := s.ClientStore() + err := clientStore.UpdateCredential("some-cloud", jujucloud.CloudCredential{ + AuthCredentials: map[string]jujucloud.Credential{ + "some-credentials": jujucloud.NewCredential(jujucloud.EmptyAuthType, nil), + }, + }) + c.Assert(err, gc.IsNil) + + _, err = cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(clientStore, bClient), + "00000000-0000-0000-0000-000000000000", + "some-cloud", + "non-existing-credential-name", + ) + c.Assert(err, gc.ErrorMatches, "credential \"non-existing-credential-name\" not found on local client.*") +} + +func (s *updateCredentialsSuite) TestMissingArgs(c *gc.C) { + tests := []struct { + name string + args []string + expectedError string + }{{ + name: "missing client ID", + args: []string{}, + expectedError: "client ID not specified", + }, { + name: "missing cloud", + args: []string{"some-client-id"}, + expectedError: "cloud not specified", + }, { + name: "missing credential name", + args: []string{"some-client-id", "some-cloud"}, + expectedError: "credential name not specified", + }, { + name: "too many args", + args: []string{"some-client-id", "some-cloud", "some-credential-name", "extra-arg"}, + expectedError: "too many args", + }} + + bClient := s.UserBakeryClient("alice") + clientStore := s.ClientStore() + for _, t := range tests { + _, err := cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(clientStore, bClient), t.args...) + c.Assert(err, gc.ErrorMatches, t.expectedError, gc.Commentf("test case failed: %q", t.name)) + } +} diff --git a/cmd/serviceaccounts/main.go b/cmd/serviceaccounts/main.go index ddb3983cd..6ccfb7f6d 100644 --- a/cmd/serviceaccounts/main.go +++ b/cmd/serviceaccounts/main.go @@ -22,6 +22,7 @@ func NewSuperCommand() *jujucmd.SuperCommand { // Register commands here: serviceAccountCmd.Register(cmd.NewAddServiceAccountCommand()) serviceAccountCmd.Register(cmd.NewListServiceAccountCredentialsCommand()) + serviceAccountCmd.Register(cmd.NewUpdateCredentialsCommand()) serviceAccountCmd.Register(cmd.NewGrantCommand()) return serviceAccountCmd } From d81b13605ab4738e178827c9e4c0a42afff77adb Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Fri, 2 Feb 2024 10:02:21 +0000 Subject: [PATCH 050/126] Plug oauth authenticator into jimm (#1144) * feat(oauth2.go): introduce JWT creation and validation for CLI sessions * feat(package doc for auth): adds a package doc for auth * feat(auth): update error code * feat(oauthauthenticator): plug in the authenticator params and create it on the JIMM service * PR comments * feat(pr fixes): pr fixes * fix * add ref * remove else * Connect directly to realm for tests * typo * Add auth to test utils * fix service tests * debug line to help fix tests in ci --- cmd/jimmctl/cmd/jimmsuite_test.go | 8 +++++ cmd/jimmsrv/main.go | 52 +++++++++++++++++++++++++++++-- docker-compose.yaml | 5 ++- internal/auth/oauth2.go | 4 +++ internal/jimm/jimm.go | 47 ++++++++++++++++++++++++++++ internal/jimmjwx/utils_test.go | 7 +++++ internal/jimmtest/suite.go | 10 ++++++ service.go | 40 +++++++++++++++++++++++- service_test.go | 50 +++++++++++++++++++++++++++++ 9 files changed, 219 insertions(+), 4 deletions(-) diff --git a/cmd/jimmctl/cmd/jimmsuite_test.go b/cmd/jimmctl/cmd/jimmsuite_test.go index f3fc0c813..f3540c245 100644 --- a/cmd/jimmctl/cmd/jimmsuite_test.go +++ b/cmd/jimmctl/cmd/jimmsuite_test.go @@ -12,6 +12,7 @@ import ( "time" cofga "github.com/canonical/ofga" + "github.com/coreos/go-oidc/v3/oidc" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" @@ -82,7 +83,14 @@ func (s *jimmSuite) SetUpTest(c *gc.C) { }, JWTExpiryDuration: time.Minute, InsecureSecretStorage: true, + OAuthAuthenticatorParams: service.OAuthAuthenticatorParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + AccessTokenExpiry: time.Duration(time.Hour), + }, } + srv, err := service.NewService(ctx, s.Params) c.Assert(err, gc.Equals, nil) s.Service = srv diff --git a/cmd/jimmsrv/main.go b/cmd/jimmsrv/main.go index 3a3831375..f2fea7512 100644 --- a/cmd/jimmsrv/main.go +++ b/cmd/jimmsrv/main.go @@ -5,6 +5,7 @@ package main import ( "context" "net/http" + "net/url" "os" "strings" "syscall" @@ -15,6 +16,7 @@ import ( "go.uber.org/zap" "github.com/canonical/jimm" + "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/version" ) @@ -57,15 +59,55 @@ func start(ctx context.Context, s *service.Service) error { macaroonExpiryDuration = expiry } jwtExpiryDuration := 24 * time.Hour - durationString = os.Getenv("JIMM_MACAROON_EXPIRY_DURATION") + durationString = os.Getenv("JIMM_JWT_EXPIRY") if durationString != "" { expiry, err := time.ParseDuration(durationString) if err != nil { - zapctx.Error(ctx, "failed to parse macaroon expiry duration", zap.Error(err)) + zapctx.Error(ctx, "failed to parse jwt expiry duration", zap.Error(err)) } else { jwtExpiryDuration = expiry } } + + accessTokenExpiryDuration := time.Duration(0) + durationString = os.Getenv("JIMM_ACCESS_TOKEN_EXPIRY_DURATION") + if durationString != "" { + expiry, err := time.ParseDuration(durationString) + if err != nil { + zapctx.Error(ctx, "failed to parse access token expiry duration", zap.Error(err)) + return err + } + accessTokenExpiryDuration = expiry + } + + issuerURL := os.Getenv("JIMM_OAUTH_ISSUER_URL") + parsedIssuerURL, err := url.Parse(issuerURL) + if err != nil { + zapctx.Error(ctx, "failed to parse oauth issuer url", zap.Error(err)) + return err + } + + if parsedIssuerURL.Scheme == "" { + zapctx.Error(ctx, "oauth issuer url has no scheme") + return errors.E("oauth issuer url has no scheme") + } + + deviceClientID := os.Getenv("JIMM_OAUTH_DEVICE_CLIENT_ID") + if deviceClientID == "" { + zapctx.Error(ctx, "no oauth device client id") + return errors.E("no oauth device client id") + } + + deviceScopes := os.Getenv("JIMM_OAUTH_DEVICE_SCOPES") + deviceScopesParsed := strings.Split(deviceScopes, ",") + for i, scope := range deviceScopesParsed { + deviceScopesParsed[i] = strings.TrimSpace(scope) + } + if len(deviceScopesParsed) == 0 { + zapctx.Error(ctx, "no oauth device client scopes present") + return errors.E("no oauth device client scopes present") + } + insecureSecretStorage := false if _, ok := os.LookupEnv("INSECURE_SECRET_STORAGE"); ok { insecureSecretStorage = true @@ -102,6 +144,12 @@ func start(ctx context.Context, s *service.Service) error { JWTExpiryDuration: jwtExpiryDuration, InsecureSecretStorage: insecureSecretStorage, InsecureJwksLookup: insecureJwksLookup, + OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ + IssuerURL: issuerURL, + DeviceClientID: deviceClientID, + DeviceScopes: deviceScopesParsed, + AccessTokenExpiry: accessTokenExpiryDuration, + }, }) if err != nil { return err diff --git a/docker-compose.yaml b/docker-compose.yaml index 64e195a24..47e95400a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -75,6 +75,9 @@ services: OPENFGA_STORE: "01GP1254CHWJC1MNGVB0WDG1T0" OPENFGA_AUTH_MODEL: "01GP1EC038KHGB6JJ2XXXXCXKB" OPENFGA_TOKEN: "jimm" + JIMM_OAUTH_ISSUER_URL: "http://keycloak:8082/realms/jimm" # Scheme required + JIMM_OAUTH_DEVICE_CLIENT_ID: "jimm-device" # Must be a public client, no client secret + JIMM_OAUTH_DEVICE_SCOPES: "openid, profile, email" # Comma separated list of scopes volumes: - ./:/jimm/ - ./local/vault/approle.json:/vault/approle.json:rw @@ -241,7 +244,7 @@ services: ports: - "8082:8082" healthcheck: - test: [ "CMD", "curl", "http://0.0.0.0:8082/health/started" ] + test: [ "CMD", "curl", "http://localhost:8082/realms/jimm/.well-known/openid-configuration" ] interval: 5s timeout: 5s retries: 30 diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 2a808f5f7..59a11cd37 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -8,8 +8,10 @@ import ( "time" "github.com/coreos/go-oidc/v3/oidc" + "github.com/juju/zaputil/zapctx" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwt" + "go.uber.org/zap" "golang.org/x/oauth2" "github.com/canonical/jimm/internal/errors" @@ -48,6 +50,7 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP provider, err := oidc.NewProvider(ctx, params.IssuerURL) if err != nil { + zapctx.Error(ctx, "failed to create oidc provider", zap.Error(err)) return nil, errors.E(op, errors.CodeServerConfiguration, err, "failed to create oidc provider") } @@ -81,6 +84,7 @@ func (as *AuthenticationService) Device(ctx context.Context) (*oauth2.DeviceAuth resp, err := as.deviceConfig.DeviceAuth(ctx) if err != nil { + zapctx.Error(ctx, "device auth call failed", zap.Error(err)) return nil, errors.E(op, err, "device auth call failed") } diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index 60fb94de3..f85186287 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/coreos/go-oidc/v3/oidc" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/juju/api/base" "github.com/juju/juju/core/crossmodel" @@ -17,6 +18,7 @@ import ( "github.com/juju/names/v4" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" + "golang.org/x/oauth2" "golang.org/x/sync/errgroup" "github.com/canonical/jimm/internal/db" @@ -76,9 +78,21 @@ type JIMM struct { // with the OpenFGA ReBAC system. OpenFGAClient *openfga.OFGAClient + // JWKService holds a service responsible for generating and delivering a JWKS + // for consumption within Juju controllers. JWKService *jimmjwx.JWKSService + // JWTService is responsible for minting JWTs to access controllers. JWTService *jimmjwx.JWTService + + // OAuthAuthenticator is responsible for handling authentication + // via OAuth2.0 AND JWT access tokens to JIMM. + OAuthAuthenticator OAuthAuthenticator +} + +// OAuthAuthenticationService returns the JIMM's authentication service. +func (j *JIMM) OAuthAuthenticationService() OAuthAuthenticator { + return j.OAuthAuthenticator } // ResourceTag returns JIMM's controller tag stating its UUID. @@ -108,6 +122,39 @@ type Authenticator interface { Authenticate(ctx context.Context, req *jujuparams.LoginRequest) (*openfga.User, error) } +// OAuthAuthenticator is responsible for handling authentication +// via OAuth2.0 AND JWT access tokens to JIMM. +type OAuthAuthenticator interface { + // Device initiates a device flow login and is step ONE of TWO. + // + // This is done via retrieving a: + // - Device code + // - User code + // - VerificationURI + // - Interval + // - Expiry + // From the device /auth endpoint. + // + // The verification uri and user code is sent to the user, as they must enter the code + // into the uri. + // + // The interval, expiry and device code and used to poll the token endpoint for completion. + Device(ctx context.Context) (*oauth2.DeviceAuthResponse, error) + + // DeviceAccessToken continues and collect an access token during the device login flow + // and is step TWO. + // + // See Device(...) godoc for more info pertaining to the flow. + DeviceAccessToken(ctx context.Context, res *oauth2.DeviceAuthResponse) (*oauth2.Token, error) + + // ExtractAndVerifyIDToken extracts the id token from the extras claims of an oauth2 token + // and performs signature verification of the token. + ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) + + // Email retrieves the users email from an id token via the email claim + Email(idToken *oidc.IDToken) (string, error) +} + type permission struct { resource string relation string diff --git a/internal/jimmjwx/utils_test.go b/internal/jimmjwx/utils_test.go index c5bbbdb45..ecc59081a 100644 --- a/internal/jimmjwx/utils_test.go +++ b/internal/jimmjwx/utils_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" "github.com/google/uuid" "github.com/lestrrat-go/jwx/v2/jwk" @@ -107,6 +108,12 @@ func setupService(ctx context.Context, c *qt.C) (*jimm.Service, *httptest.Server Token: cofgaParams.Token, AuthModel: cofgaParams.AuthModelID, }, + OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + AccessTokenExpiry: time.Duration(time.Hour), + }, }) c.Assert(err, qt.IsNil) diff --git a/internal/jimmtest/suite.go b/internal/jimmtest/suite.go index 76c909d78..926cd64b0 100644 --- a/internal/jimmtest/suite.go +++ b/internal/jimmtest/suite.go @@ -10,6 +10,7 @@ import ( "github.com/canonical/candid/candidtest" cofga "github.com/canonical/ofga" + "github.com/coreos/go-oidc/v3/oidc" "github.com/go-chi/chi/v5" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" @@ -86,6 +87,15 @@ func (s *JIMMSuite) SetUpTest(c *gc.C) { ctx, cancel := context.WithCancel(context.Background()) s.cancel = cancel + // Connects to a pre-configured keycloak realm + authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + }) + c.Assert(err, gc.Equals, nil) + s.JIMM.OAuthAuthenticator = authSvc + err = s.JIMM.Database.Migrate(ctx, false) c.Assert(err, gc.Equals, nil) s.AdminUser = &dbmodel.User{ diff --git a/service.go b/service.go index 02c6facfd..d269f033b 100644 --- a/service.go +++ b/service.go @@ -63,6 +63,23 @@ type OpenFGAParams struct { Port string } +// OAuthAuthenticatorParams holds parameters needed to configure an OAuthAuthenticator +// implementation. +type OAuthAuthenticatorParams struct { + // IssuerURL is the URL of the OAuth2.0 server. + // I.e., http://localhost:8082/realms/jimm in the case of keycloak. + IssuerURL string + // DeviceClientID holds the OAuth2.0 client id registered and configured + // to handle device OAuth2.0 flows. The client is NOT expected to be confidential + // and as such does not need a client secret (given it is configured correctly). + DeviceClientID string + // DeviceScopes holds the scopes that you wish to retrieve. + DeviceScopes []string + // AccessTokenExpiry holds the expiry duration for issued JWTs + // for user (CLI) to JIMM authentication. + AccessTokenExpiry time.Duration +} + // A Params structure contains the parameters required to initialise a new // Service. type Params struct { @@ -152,7 +169,8 @@ type Params struct { // MacaroonExpiryDuration holds the expiry duration of authentication macaroons. MacaroonExpiryDuration time.Duration - // JWTExpiryDuration holds the expiry duration for issued JWTs. + // JWTExpiryDuration holds the expiry duration for issued JWTs + // for controller to JIMM communication ONLY. JWTExpiryDuration time.Duration // InsecureSecretStorage instructs JIMM to store secrets in its database @@ -162,6 +180,10 @@ type Params struct { // InsecureJwksLookup instructs JIMM to lookup its JWKS value via // http instead of https. Useful when running JIMM in a docker compose. InsecureJwksLookup bool + + // OAuthAuthenticatorParams holds parameters needed to configure an OAuthAuthenticator + // implementation. + OAuthAuthenticatorParams OAuthAuthenticatorParams } // A Service is the implementation of a JIMM server. @@ -289,11 +311,27 @@ func NewService(ctx context.Context, p Params) (*Service, error) { } s.mux.Handle(localDischargePath+"/*", dischargeMux) + // Ale8k: This authenticator is old and used for macaroon auth + // it is still present for backwards compatibility but SHOULD + // be removed in the future. s.jimm.Authenticator, err = newAuthenticator(ctx, &s.jimm.Database, openFGAclient, kp, p) if err != nil { return nil, errors.E(op, err) } + s.jimm.OAuthAuthenticator, err = auth.NewAuthenticationService( + ctx, + auth.AuthenticationServiceParams{ + IssuerURL: p.OAuthAuthenticatorParams.IssuerURL, + DeviceClientID: p.OAuthAuthenticatorParams.DeviceClientID, + DeviceScopes: p.OAuthAuthenticatorParams.DeviceScopes, + }, + ) + if err != nil { + zapctx.Error(ctx, "failed to setup authentication service", zap.Error(err)) + return nil, errors.E(op, err, "failed to setup authentication service") + } + if err := s.setupCredentialStore(ctx, p); err != nil { return nil, errors.E(op, err) } diff --git a/service_test.go b/service_test.go index 5827e6a16..19f854f46 100644 --- a/service_test.go +++ b/service_test.go @@ -12,9 +12,11 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/canonical/candid/candidtest" cofga "github.com/canonical/ofga" + "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" @@ -48,6 +50,12 @@ func TestDefaultService(t *testing.T) { DSN: jimmtest.CreateEmptyDatabase(c), OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), InsecureSecretStorage: true, + OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + AccessTokenExpiry: time.Duration(time.Hour), + }, }) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() @@ -66,6 +74,12 @@ func TestServiceStartsWithoutSecretStore(t *testing.T) { _, err = jimm.NewService(context.Background(), jimm.Params{ DSN: jimmtest.CreateEmptyDatabase(c), OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), + OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + AccessTokenExpiry: time.Duration(time.Hour), + }, }) c.Assert(err, qt.IsNil) } @@ -82,6 +96,12 @@ func TestAuthenticator(t *testing.T) { ControllerAdmins: []string{"admin"}, OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), InsecureSecretStorage: true, + OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + AccessTokenExpiry: time.Duration(time.Hour), + }, } candid := startCandid(c, &p) svc, err := jimm.NewService(context.Background(), p) @@ -145,6 +165,12 @@ func TestVault(t *testing.T) { VaultPath: "/jimm-kv/", VaultSecretFile: "./local/vault/approle.json", OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), + OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + AccessTokenExpiry: time.Duration(time.Hour), + }, } candid := startCandid(c, &p) vaultClient, _, creds, _ := jimmtest.VaultClient(c, ".") @@ -208,6 +234,12 @@ func TestPostgresSecretStore(t *testing.T) { DSN: jimmtest.CreateEmptyDatabase(c), OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), InsecureSecretStorage: true, + OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + AccessTokenExpiry: time.Duration(time.Hour), + }, } _, err = jimm.NewService(context.Background(), p) c.Assert(err, qt.IsNil) @@ -224,6 +256,12 @@ func TestOpenFGA(t *testing.T) { DSN: jimmtest.CreateEmptyDatabase(c), OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), ControllerAdmins: []string{"alice", "eve"}, + OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + AccessTokenExpiry: time.Duration(time.Hour), + }, } candid := startCandid(c, &p) svc, err := jimm.NewService(context.Background(), p) @@ -274,6 +312,12 @@ func TestPublicKey(t *testing.T) { ControllerAdmins: []string{"alice", "eve"}, PrivateKey: "c1VkV05+iWzCxMwMVcWbr0YJWQSEO62v+z3EQ2BhFMw=", PublicKey: "pC8MEk9MS9S8fhyRnOJ4qARTcTAwoM9L1nH/Yq0MwWU=", + OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + AccessTokenExpiry: time.Duration(time.Hour), + }, } _ = startCandid(c, &p) svc, err := jimm.NewService(context.Background(), p) @@ -356,6 +400,12 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { ControllerAdmins: []string{"alice", "eve"}, PrivateKey: "c1VkV05+iWzCxMwMVcWbr0YJWQSEO62v+z3EQ2BhFMw=", PublicKey: "pC8MEk9MS9S8fhyRnOJ4qARTcTAwoM9L1nH/Yq0MwWU=", + OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + AccessTokenExpiry: time.Duration(time.Hour), + }, } _ = startCandid(c, &p) svc, err := jimm.NewService(context.Background(), p) From 463e211248a6dcf074e979655a46fc3be0d83d2a Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 2 Feb 2024 12:19:54 +0200 Subject: [PATCH 051/126] CSS-6973 Create juju jaas snap (#1149) * Add jaas snap Move service account cli package to jaas * Update snapcraft.yaml * Move to bare base * Rename rebased test imports --- Makefile | 5 +++ .../cmd/addserviceaccount.go | 0 .../cmd/addserviceaccount_test.go | 2 +- .../cmd/export_test.go | 0 cmd/{serviceaccounts => jaas}/cmd/grant.go | 0 .../cmd/grant_test.go | 2 +- .../cmd/listserviceaccountcredentials.go | 0 .../cmd/listserviceaccountcredentials_test.go | 2 +- .../cmd/package_test.go | 0 .../cmd/updatecredentials.go | 0 .../cmd/updatecredentials_test.go | 2 +- cmd/{serviceaccounts => jaas}/main.go | 14 +++++--- snaps/jaas/snapcraft.yaml | 33 +++++++++++++++++++ 13 files changed, 51 insertions(+), 9 deletions(-) rename cmd/{serviceaccounts => jaas}/cmd/addserviceaccount.go (100%) rename cmd/{serviceaccounts => jaas}/cmd/addserviceaccount_test.go (96%) rename cmd/{serviceaccounts => jaas}/cmd/export_test.go (100%) rename cmd/{serviceaccounts => jaas}/cmd/grant.go (100%) rename cmd/{serviceaccounts => jaas}/cmd/grant_test.go (98%) rename cmd/{serviceaccounts => jaas}/cmd/listserviceaccountcredentials.go (100%) rename cmd/{serviceaccounts => jaas}/cmd/listserviceaccountcredentials_test.go (98%) rename cmd/{serviceaccounts => jaas}/cmd/package_test.go (100%) rename cmd/{serviceaccounts => jaas}/cmd/updatecredentials.go (100%) rename cmd/{serviceaccounts => jaas}/cmd/updatecredentials_test.go (99%) rename cmd/{serviceaccounts => jaas}/main.go (71%) create mode 100644 snaps/jaas/snapcraft.yaml diff --git a/Makefile b/Makefile index 89f5d0411..fe081ab65 100644 --- a/Makefile +++ b/Makefile @@ -88,6 +88,11 @@ jimmctl-snap: cp -R ./snaps/jimmctl/* ./snap/ snapcraft +jaas-snap: + mkdir -p ./snap + cp -R ./snaps/jaas/* ./snap/ + snapcraft + push-microk8s: jimm-image docker tag jimm:latest localhost:32000/jimm:latest docker push localhost:32000/jimm:latest diff --git a/cmd/serviceaccounts/cmd/addserviceaccount.go b/cmd/jaas/cmd/addserviceaccount.go similarity index 100% rename from cmd/serviceaccounts/cmd/addserviceaccount.go rename to cmd/jaas/cmd/addserviceaccount.go diff --git a/cmd/serviceaccounts/cmd/addserviceaccount_test.go b/cmd/jaas/cmd/addserviceaccount_test.go similarity index 96% rename from cmd/serviceaccounts/cmd/addserviceaccount_test.go rename to cmd/jaas/cmd/addserviceaccount_test.go index 431b1a96a..88ea8fbd2 100644 --- a/cmd/serviceaccounts/cmd/addserviceaccount_test.go +++ b/cmd/jaas/cmd/addserviceaccount_test.go @@ -9,7 +9,7 @@ import ( "github.com/juju/names/v4" gc "gopkg.in/check.v1" - "github.com/canonical/jimm/cmd/serviceaccounts/cmd" + "github.com/canonical/jimm/cmd/jaas/cmd" "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/openfga" ofganames "github.com/canonical/jimm/internal/openfga/names" diff --git a/cmd/serviceaccounts/cmd/export_test.go b/cmd/jaas/cmd/export_test.go similarity index 100% rename from cmd/serviceaccounts/cmd/export_test.go rename to cmd/jaas/cmd/export_test.go diff --git a/cmd/serviceaccounts/cmd/grant.go b/cmd/jaas/cmd/grant.go similarity index 100% rename from cmd/serviceaccounts/cmd/grant.go rename to cmd/jaas/cmd/grant.go diff --git a/cmd/serviceaccounts/cmd/grant_test.go b/cmd/jaas/cmd/grant_test.go similarity index 98% rename from cmd/serviceaccounts/cmd/grant_test.go rename to cmd/jaas/cmd/grant_test.go index 66a851392..d50e4c703 100644 --- a/cmd/serviceaccounts/cmd/grant_test.go +++ b/cmd/jaas/cmd/grant_test.go @@ -9,7 +9,7 @@ import ( "github.com/juju/names/v4" gc "gopkg.in/check.v1" - "github.com/canonical/jimm/cmd/serviceaccounts/cmd" + "github.com/canonical/jimm/cmd/jaas/cmd" "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/openfga" diff --git a/cmd/serviceaccounts/cmd/listserviceaccountcredentials.go b/cmd/jaas/cmd/listserviceaccountcredentials.go similarity index 100% rename from cmd/serviceaccounts/cmd/listserviceaccountcredentials.go rename to cmd/jaas/cmd/listserviceaccountcredentials.go diff --git a/cmd/serviceaccounts/cmd/listserviceaccountcredentials_test.go b/cmd/jaas/cmd/listserviceaccountcredentials_test.go similarity index 98% rename from cmd/serviceaccounts/cmd/listserviceaccountcredentials_test.go rename to cmd/jaas/cmd/listserviceaccountcredentials_test.go index 684c0288a..cf8e3bd59 100644 --- a/cmd/serviceaccounts/cmd/listserviceaccountcredentials_test.go +++ b/cmd/jaas/cmd/listserviceaccountcredentials_test.go @@ -12,7 +12,7 @@ import ( "github.com/juju/names/v4" gc "gopkg.in/check.v1" - "github.com/canonical/jimm/cmd/serviceaccounts/cmd" + "github.com/canonical/jimm/cmd/jaas/cmd" "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/jimm" diff --git a/cmd/serviceaccounts/cmd/package_test.go b/cmd/jaas/cmd/package_test.go similarity index 100% rename from cmd/serviceaccounts/cmd/package_test.go rename to cmd/jaas/cmd/package_test.go diff --git a/cmd/serviceaccounts/cmd/updatecredentials.go b/cmd/jaas/cmd/updatecredentials.go similarity index 100% rename from cmd/serviceaccounts/cmd/updatecredentials.go rename to cmd/jaas/cmd/updatecredentials.go diff --git a/cmd/serviceaccounts/cmd/updatecredentials_test.go b/cmd/jaas/cmd/updatecredentials_test.go similarity index 99% rename from cmd/serviceaccounts/cmd/updatecredentials_test.go rename to cmd/jaas/cmd/updatecredentials_test.go index 8f69684fe..7c158dc07 100644 --- a/cmd/serviceaccounts/cmd/updatecredentials_test.go +++ b/cmd/jaas/cmd/updatecredentials_test.go @@ -9,7 +9,7 @@ import ( "github.com/juju/names/v4" gc "gopkg.in/check.v1" - "github.com/canonical/jimm/cmd/serviceaccounts/cmd" + "github.com/canonical/jimm/cmd/jaas/cmd" "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/openfga" diff --git a/cmd/serviceaccounts/main.go b/cmd/jaas/main.go similarity index 71% rename from cmd/serviceaccounts/main.go rename to cmd/jaas/main.go index 6ccfb7f6d..96eecb632 100644 --- a/cmd/serviceaccounts/main.go +++ b/cmd/jaas/main.go @@ -6,18 +6,22 @@ import ( "fmt" "os" - "github.com/canonical/jimm/cmd/serviceaccounts/cmd" + "github.com/canonical/jimm/cmd/jaas/cmd" jujucmd "github.com/juju/cmd/v3" ) -var serviceAccountDoc = ` -juju service-accounts enables users to manage service accounts. +var jaasDoc = ` +juju jaas enables users to use JAAS commands from within Juju. + +JAAS enables enterprise functionality on top of Juju to provide +functionality like OIDC login, control over many controllers, +and fine-grained authorisation. ` func NewSuperCommand() *jujucmd.SuperCommand { serviceAccountCmd := jujucmd.NewSuperCommand(jujucmd.SuperCommandParams{ - Name: "service-accounts", - Doc: serviceAccountDoc, + Name: "jaas", + Doc: jaasDoc, }) // Register commands here: serviceAccountCmd.Register(cmd.NewAddServiceAccountCommand()) diff --git a/snaps/jaas/snapcraft.yaml b/snaps/jaas/snapcraft.yaml new file mode 100644 index 000000000..34ada0bc9 --- /dev/null +++ b/snaps/jaas/snapcraft.yaml @@ -0,0 +1,33 @@ +name: juju-jaas +summary: JAAS plugin +description: Juju plugin for providing JAAS functionality to the Juju CLI. +version: git +grade: stable +base: bare +build-base: core20 +confinement: strict + +slots: + jaas-plugin: + interface: content + content: jaas-plugin + read: + - $SNAP/bin + +# The app has no plugs as it is intended to be invoked by the Juju CLI Snap. +apps: + jaas: + command: bin/juju-jaas + +parts: + jaas: + plugin: go + source: ./ + source-type: git + prime: + - bin/juju-jaas + override-build: | + set -e + CGO_ENABLED=0 go build -o juju-jaas github.com/canonical/jimm/cmd/jaas + mkdir -p $GOBIN + cp ./juju-jaas $GOBIN/juju-jaas From d1d284f80f8e3c41af95b0ec2815c4e974c97cc4 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 2 Feb 2024 13:41:57 +0200 Subject: [PATCH 052/126] Make workflow reusable (#1150) Rename juju-jaas plugin to jaas make snap release more generic --- .github/workflows/jaas-snap-release.yaml | 14 ++++++++++++++ .github/workflows/jimmctl-snap-release.yaml | 15 +++++++++++++++ .github/workflows/snap-release.yaml | 19 +++++++++++-------- snaps/jaas/snapcraft.yaml | 2 +- 4 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/jaas-snap-release.yaml create mode 100644 .github/workflows/jimmctl-snap-release.yaml diff --git a/.github/workflows/jaas-snap-release.yaml b/.github/workflows/jaas-snap-release.yaml new file mode 100644 index 000000000..96a308c37 --- /dev/null +++ b/.github/workflows/jaas-snap-release.yaml @@ -0,0 +1,14 @@ +name: Release jimmctl snap + +on: + workflow_dispatch: + push: + tags: + - 'v3*' + +jobs: + build-and-release: + uses: ./.github/workflows/snap-release.yaml + with: + folder: jaas + release-channel: 3/edge diff --git a/.github/workflows/jimmctl-snap-release.yaml b/.github/workflows/jimmctl-snap-release.yaml new file mode 100644 index 000000000..2a5cc9c8e --- /dev/null +++ b/.github/workflows/jimmctl-snap-release.yaml @@ -0,0 +1,15 @@ +name: Release jimmctl snap + +on: + workflow_dispatch: + push: + tags: + - 'v3*' + +jobs: + build-and-release: + uses: ./.github/workflows/snap-release.yaml + with: + folder: jimmctl + release-channel: 3/edge + diff --git a/.github/workflows/snap-release.yaml b/.github/workflows/snap-release.yaml index ae2cd3c32..00c1fd6e2 100644 --- a/.github/workflows/snap-release.yaml +++ b/.github/workflows/snap-release.yaml @@ -1,10 +1,14 @@ -name: Release jimmctl snap +name: Release snap on: - workflow_dispatch: - push: - tags: - - 'v3*' + workflow_call: + inputs: + folder: + required: true + type: string + release-channel: + required: true + type: string # Note this workflow requires a Github secret to provide auth against snapstore. # snapcraft export-login --snaps=PACKAGE_NAME --acls package_access,package_push,package_update,package_release exported.txt @@ -23,7 +27,7 @@ jobs: - name: scripts run: | mkdir -p ./snap - cp ./snaps/jimmctl/snapcraft.yaml ./snap/ + cp ./snaps/${{ inputs.folder }}/snapcraft.yaml ./snap/ - uses: snapcore/action-build@v1 id: snapcraft - uses: actions/upload-artifact@v2 @@ -44,5 +48,4 @@ jobs: with: store_login: ${{ secrets.STORE_LOGIN }} snap: ${{needs.build.outputs.snap}} - release: '3/edge' - + release: '${{ inputs.release-channel }}' diff --git a/snaps/jaas/snapcraft.yaml b/snaps/jaas/snapcraft.yaml index 34ada0bc9..3c596ea60 100644 --- a/snaps/jaas/snapcraft.yaml +++ b/snaps/jaas/snapcraft.yaml @@ -1,4 +1,4 @@ -name: juju-jaas +name: jaas summary: JAAS plugin description: Juju plugin for providing JAAS functionality to the Juju CLI. version: git From 0800d030ec9142a75408f5473ba1751dec13f80d Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 5 Feb 2024 12:09:17 +0000 Subject: [PATCH 053/126] CSS-7048 Create Keycloak users in tests (#1153) * Enable credentials for `admin-cli` client Signed-off-by: Babak K. Shandiz * Add `create-user.sh` Signed-off-by: Babak K. Shandiz * Add Keycloak methods to `jimmtest` package Signed-off-by: Babak K. Shandiz * Update test to create Keycloak user per test Signed-off-by: Babak K. Shandiz * Move exported symbols to top Signed-off-by: Babak K. Shandiz * Add godocs Signed-off-by: Babak K. Shandiz * Fix misspelling in file name Signed-off-by: Babak K. Shandiz * Fix test container setup Signed-off-by: Babak K. Shandiz --------- Signed-off-by: Babak K. Shandiz --- .github/workflows/ci.yaml | 2 +- docker-compose.yaml | 10 +- internal/auth/oauth2_test.go | 14 +- internal/jimmtest/keycloak.go | 259 +++++++++++++++++++++++++++++++++ local/keycloak/create-user.sh | 54 +++++++ local/keycloak/jimm-realm.json | 23 ++- 6 files changed, 348 insertions(+), 14 deletions(-) create mode 100644 internal/jimmtest/keycloak.go create mode 100755 local/keycloak/create-user.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bb1f69d23..0f32ab693 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,7 +43,7 @@ jobs: touch ./local/vault/approle.json touch ./local/vault/roleid.txt - name: Start test environment - run: docker compose up -d + run: docker compose up -d --wait - name: Build and Test run: go test -mod readonly ./... -timeout 1h -cover env: diff --git a/docker-compose.yaml b/docker-compose.yaml index 47e95400a..2a1d149c4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -217,13 +217,17 @@ services: # Adds the auth model and updates its authorisation model id to be the expected hard-coded id such that our local JIMM can utilise it for queries. # The auth model json is retrieved from file via volume mount. insert-hardcoded-auth-model: + profiles: ["dev"] image: governmentpaas/psql container_name: insert-hardcoded-auth-model volumes: - ./local/openfga/authorisation_model.json:/authorisation_model.json - command: > - sh -c "wget -q -O - --header 'Content-Type: application/json' --header 'Authorization: Bearer jimm' --post-file authorisation_model.json openfga:8080/stores/01GP1254CHWJC1MNGVB0WDG1T0/authorization-models && \ - psql -Atx postgresql://jimm:jimm@db/jimm?sslmode=disable -c \"UPDATE authorization_model SET authorization_model_id = '01GP1EC038KHGB6JJ2XXXXCXKB' WHERE store = '01GP1254CHWJC1MNGVB0WDG1T0';\"" + command: + - /bin/sh + - -c + - | + wget -q -O - --header 'Content-Type: application/json' --header 'Authorization: Bearer jimm' --post-file authorisation_model.json openfga:8080/stores/01GP1254CHWJC1MNGVB0WDG1T0/authorization-models + psql -Atx postgresql://jimm:jimm@db/jimm?sslmode=disable -c "UPDATE authorization_model SET authorization_model_id = '01GP1EC038KHGB6JJ2XXXXCXKB' WHERE store = '01GP1254CHWJC1MNGVB0WDG1T0';" depends_on: openfga: condition: service_healthy diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 21fb7f1ea..ac8d3ea0c 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/canonical/jimm/internal/auth" + "github.com/canonical/jimm/internal/jimmtest" "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" ) @@ -29,6 +30,9 @@ import ( func TestDevice(t *testing.T) { c := qt.New(t) + u, err := jimmtest.CreateRandomKeycloakUser() + c.Assert(err, qt.IsNil) + ctx := context.Background() authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ @@ -64,10 +68,8 @@ func TestDevice(t *testing.T) { loginFormUrl := match[1] v := url.Values{} - // The username and password are hardcoded witih jimm-realm.json in our local - // keycloak configuration for the jimm realm. - v.Add("username", "jimm-test") - v.Add("password", "password") + v.Add("username", u.Username) + v.Add("password", u.Password) loginResp, err := client.PostForm(loginFormUrl, v) c.Assert(err, qt.IsNil) defer loginResp.Body.Close() @@ -103,12 +105,12 @@ func TestDevice(t *testing.T) { c.Assert(idToken, qt.IsNotNil) // Test subject set - c.Assert(idToken.Subject, qt.Equals, "8281cec3-5b48-46eb-a41d-72c15ec3f9e0") + c.Assert(idToken.Subject, qt.Equals, u.Id) // Retrieve the email email, err := authSvc.Email(idToken) c.Assert(err, qt.IsNil) - c.Assert(email, qt.Equals, "jimm-test@canonical.com") + c.Assert(email, qt.Equals, u.Email) } // TestAccessTokens tests both the minting and validation of JIMM diff --git a/internal/jimmtest/keycloak.go b/internal/jimmtest/keycloak.go new file mode 100644 index 000000000..235486a38 --- /dev/null +++ b/internal/jimmtest/keycloak.go @@ -0,0 +1,259 @@ +// Copyright 2024 Canonical Ltd. + +package jimmtest + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/canonical/jimm/internal/errors" + "github.com/google/uuid" +) + +// These constants are based on the `docker-compose.yaml` and `local/keycloak/jimm-realm.json` content. +const ( + keycloakHost = "localhost:8082" + keycloakJIMMRealmPath = "/admin/realms/jimm" + keycloakAdminUsername = "jimm" + keycloakAdminPassword = "jimm" + keycloakAdminCLIUsername = "admin-cli" + keycloakAdminCLISecret = "DOLcuE5Cd7IxuR7JE4hpAUxaLF7RlAWh" +) + +// KeycloakUser represents a basic user created in Keycloak. +type KeycloakUser struct { + Id string + Email string + Username string + Password string +} + +// CreateRandomKeycloakUser creates a Keycloak user with random username and +// returns the created user details. +func CreateRandomKeycloakUser() (*KeycloakUser, error) { + username := "random_user_" + uuid.New().String()[0:8] + email := username + "@canonical.com" + password := "jimm" + + adminCLIToken, err := getAdminCLIAccessToken() + if err != nil { + return nil, errors.E(err, "failed to authenticate admin CLI user") + } + + if err := addKeycloakUser(adminCLIToken, email, username); err != nil { + return nil, errors.E(err, fmt.Sprintf("failed to add keycloak user (%q, %q)", email, username)) + } + + id, err := getKeycloakUserId(adminCLIToken, username) + if err != nil { + return nil, errors.E(err, fmt.Sprintf("failed to retrieve ID for newly added keycloak user (%q, %q)", email, username)) + } + + if err := setKeycloakUserPassword(adminCLIToken, id, password); err != nil { + return nil, errors.E(err, fmt.Sprintf("failed to set password for newly added keycloak user (%q, %q, %q)", email, username, password)) + } + return &KeycloakUser{ + Id: id, + Email: email, + Username: username, + Password: password, + }, nil +} + +// getAdminCLIAccessToken authenticates with the `admin-cli` client and returns +// the access token to be used to communicate with Keycloak admin API. +func getAdminCLIAccessToken() (string, error) { + httpClient := http.Client{} + u := url.URL{ + Scheme: "http", + Host: keycloakHost, + User: url.UserPassword(keycloakAdminCLIUsername, keycloakAdminCLISecret), + Path: "/realms/master/protocol/openid-connect/token", + } + reqBody := url.Values{} + reqBody.Set("username", keycloakAdminUsername) + reqBody.Set("password", keycloakAdminPassword) + reqBody.Set("grant_type", "password") + resp, err := httpClient.Post( + u.String(), + "application/x-www-form-urlencoded", + strings.NewReader(reqBody.Encode()), + ) + if err != nil { + return "", errors.E(err, "failed to login with keycloak admin CLI user") + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", errors.E(err, fmt.Sprintf("failed to read keycloak response for admin CLI login (status-code: %d)", resp.StatusCode)) + } + if resp.StatusCode != http.StatusOK { + return "", errors.E(fmt.Sprintf("failed to login with keycloak admin CLI user (status-code: %d): %q", resp.StatusCode, string(body))) + } + + m := map[string]any{} + if err := json.Unmarshal(body, &m); err != nil { + return "", errors.E(err, fmt.Sprintf("failed to parse keycloak response for admin CLI login: %q", string(body))) + } + + if _, ok := m["access_token"]; !ok { + return "", errors.E(err, fmt.Sprintf("cannot find access token in keycloak response: %q", string(body))) + } + if token, ok := m["access_token"].(string); !ok { + return "", errors.E(err, fmt.Sprintf("received token is not string: %v", m["access_token"])) + } else { + return token, nil + } +} + +// getKeycloakUsersMap returns a map of Keycloak users, associating usernames to IDs. +func getKeycloakUsersMap(adminCLIToken string) (map[string]string, error) { + httpClient := http.Client{} + u := url.URL{ + Scheme: "http", + Host: keycloakHost, + Path: keycloakJIMMRealmPath + "/users", + } + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", "Bearer "+adminCLIToken) + req.Header.Add("Content-Type", "application/json") + resp, err := httpClient.Do(req) + if err != nil { + return nil, errors.E(err, "failed to get users from keycloak") + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.E(err, fmt.Sprintf("failed to read keycloak response for list of users (status-code: %d)", resp.StatusCode)) + } + if resp.StatusCode != http.StatusOK { + return nil, errors.E(fmt.Sprintf("failed to get users from keycloak (status-code: %d): %q", resp.StatusCode, string(body))) + } + + var raw []struct { + Id string `json:"id"` + Username string `json:"username"` + } + if err := json.Unmarshal(body, &raw); err != nil { + return nil, errors.E(err, fmt.Sprintf("failed to parse keycloak response for list of users: %q", string(body))) + } + + result := map[string]string{} + for _, entry := range raw { + result[entry.Username] = entry.Id + } + return result, nil +} + +// getKeycloakUserId returns the Keycloak user ID of a given username. +func getKeycloakUserId(adminCLIToken, username string) (string, error) { + m, err := getKeycloakUsersMap(adminCLIToken) + if err != nil { + return "", err + } + + if id, ok := m[username]; !ok { + return "", errors.E(fmt.Sprintf("keycloak user not found: %q", username)) + } else { + return id, nil + } +} + +// addKeycloakUser adds a user (username/email pair) to Keycloak. +func addKeycloakUser(adminCLIToken, email, username string) error { + httpClient := http.Client{} + u := url.URL{ + Scheme: "http", + Host: keycloakHost, + Path: keycloakJIMMRealmPath + "/users", + } + + reqBody := map[string]any{ + "username": username, + "email": email, + "emailVerified": true, + "enabled": true, + "realmRoles": []string{"user", "offline_access"}, + } + + reqBodyJSON, err := json.Marshal(reqBody) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(reqBodyJSON)) + if err != nil { + return err + } + + req.Header.Add("Authorization", "Bearer "+adminCLIToken) + req.Header.Add("Content-Type", "application/json") + resp, err := httpClient.Do(req) + if err != nil { + return errors.E(err, "failed to add user to keycloak") + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return errors.E(err, fmt.Sprintf("failed to read keycloak response to add user (status-code: %d)", resp.StatusCode)) + } + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return errors.E(fmt.Sprintf("failed to add user to keycloak (status-code: %d): %q", resp.StatusCode, string(body))) + } + return nil +} + +// setKeycloakUserPassword sets the password for given Keycloak user (identified by its ID). +func setKeycloakUserPassword(adminCLIToken, id, password string) error { + httpClient := http.Client{} + u := url.URL{ + Scheme: "http", + Host: keycloakHost, + Path: fmt.Sprintf("admin/realms/jimm/users/%s/reset-password", id), + } + + reqBody := map[string]any{ + "type": "password", + "temporary": false, + "value": password, + } + + reqBodyJSON, err := json.Marshal(reqBody) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(reqBodyJSON)) + if err != nil { + return err + } + + req.Header.Add("Authorization", "Bearer "+adminCLIToken) + req.Header.Add("Content-Type", "application/json") + resp, err := httpClient.Do(req) + if err != nil { + return errors.E(err, "failed to set keycloak user password") + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return errors.E(err, fmt.Sprintf("failed to read keycloak response to set user password (status-code: %d)", resp.StatusCode)) + } + if resp.StatusCode != http.StatusNoContent { + return errors.E(fmt.Sprintf("failed to set keycloak user password (status-code: %d): %q", resp.StatusCode, string(body))) + } + return nil +} diff --git a/local/keycloak/create-user.sh b/local/keycloak/create-user.sh new file mode 100755 index 000000000..16cf7696f --- /dev/null +++ b/local/keycloak/create-user.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +# Create a user in Keycloak. +# +# Usage: +# +# create-user.sh [ [ []]] +# + +username="${1:-someone}" +password="${2:-jimm}" +email="${3:-"${username}@canonical.com"}" + +access_token=$(curl -k \ + -X POST \ + http://localhost:8082/realms/master/protocol/openid-connect/token \ + --user admin-cli:DOLcuE5Cd7IxuR7JE4hpAUxaLF7RlAWh \ + -H 'content-type: application/x-www-form-urlencoded' \ + -d "username=jimm&password=jimm&grant_type=password" \ + 2>/dev/null \ + | jq --raw-output '.access_token') + +echo "Access token for admin-cli client:" +echo "$access_token" + +curl -k \ + -X POST \ + http://localhost:8082/admin/realms/jimm/users \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $access_token" \ + --data "{ \"username\": \"$username\", \"email\": \"$email\", \"emailVerified\":true, \"enabled\": true, \"realmRoles\": [ \"user\", \"offline_access\" ] }" \ + 2>/dev/null + +user_id="$(curl -k \ + -X GET \ + http://localhost:8082/admin/realms/jimm/users \ + -H "Authorization: Bearer $access_token" \ + 2>/dev/null \ + | jq --raw-output ".[] | select(.username==\"$username\") | .id")" + +curl -k \ + -X PUT \ + http://localhost:8082/admin/realms/jimm/users/$user_id/reset-password \ + -H "Content-Type: application/json" \ + -H "Authorization: bearer $access_token" \ + --data "{ \"type\": \"password\", \"temporary\": false, \"value\": \"$password\" }" \ + 2>/dev/null + +echo +echo "Created user:" +echo "ID: $user_id" +echo "Email: $email" +echo "Username: $username" +echo "Password: $password" diff --git a/local/keycloak/jimm-realm.json b/local/keycloak/jimm-realm.json index c5ee839fb..b2fb73b2c 100644 --- a/local/keycloak/jimm-realm.json +++ b/local/keycloak/jimm-realm.json @@ -593,13 +593,17 @@ ] }, { - "id": "5c17b099-c66d-4e74-991e-0fd22cb3050e", "clientId": "admin-cli", "name": "${client_admin-cli}", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", + "secret": "DOLcuE5Cd7IxuR7JE4hpAUxaLF7RlAWh", "redirectUris": [], "webOrigins": [], "notBefore": 0, @@ -609,11 +613,17 @@ "implicitFlowEnabled": false, "directAccessGrantsEnabled": true, "serviceAccountsEnabled": false, - "publicClient": true, + "publicClient": false, "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { - "post.logout.redirect.uris": "+" + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1706880117", + "backchannel.logout.session.required": "true", + "post.logout.redirect.uris": "+", + "oauth2.device.authorization.grant.enabled": "false", + "display.on.consent.screen": "false", + "backchannel.logout.revoke.offline.tokens": "false" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": false, @@ -630,7 +640,12 @@ "phone", "offline_access", "microprofile-jwt" - ] + ], + "access": { + "view": true, + "configure": true, + "manage": true + } }, { "id": "a335192a-14e7-4f47-ae24-d9b45a89fb77", From 0dc42af1672805bbf1313ed036a3cb8f61af3513 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:12:31 +0000 Subject: [PATCH 054/126] Css 6714/implement login device (#1151) * feat(logindevice): implements LoginDevice Login device now stores the oauth response on the controllerroot such that it can be shared with the second step of the flow, GetDeviceAccessToken upon a successful login from a user. It is expected that the same websocket be used for both facade calls. This prevents the need to associate a LoginDevice call to a GetDeviceAccessToken. In addition to this, the library actually expects the oauth response from device to be sent to deviceaccesstoken. 6714 * PR comments * pr comments * Wait on ci so all containers healthy * Fix CI * fix * remove "finished" from dc * feat(oauth2.go): introduce JWT creation and validation for CLI sessions (#1142) * feat(oauth2.go): introduce JWT creation and validation for CLI sessions * feat(package doc for auth): adds a package doc for auth * feat(auth): update error code * PR comments * Plug oauth authenticator into jimm (#1144) * feat(oauth2.go): introduce JWT creation and validation for CLI sessions * feat(package doc for auth): adds a package doc for auth * feat(auth): update error code * feat(oauthauthenticator): plug in the authenticator params and create it on the JIMM service * PR comments * feat(pr fixes): pr fixes * fix * add ref * remove else * Connect directly to realm for tests * typo * Add auth to test utils * fix service tests * debug line to help fix tests in ci * PR comments * pr comments * Wait on ci so all containers healthy * Fix CI * fix * remove "finished" from dc * feat(remove req param for logindevice): remove param --------- Co-authored-by: Ales Stimec --- api/params/params.go | 19 +++++ cmd/jimmctl/cmd/jimmsuite_test.go | 8 +-- cmd/jimmsrv/main.go | 12 ++-- internal/auth/oauth2.go | 16 ++--- internal/auth/oauth2_test.go | 40 +++++------ internal/jimmjwx/utils_test.go | 8 +-- internal/jujuapi/admin.go | 24 +++++++ internal/jujuapi/admin_test.go | 109 +++++++++++++++++++++++++++++ internal/jujuapi/controllerroot.go | 9 +++ service.go | 4 +- service_test.go | 64 ++++++++--------- 11 files changed, 237 insertions(+), 76 deletions(-) diff --git a/api/params/params.go b/api/params/params.go index 0d1f9a240..0b4f36fc4 100644 --- a/api/params/params.go +++ b/api/params/params.go @@ -393,3 +393,22 @@ type MigrateModelInfo struct { type MigrateModelRequest struct { Specs []MigrateModelInfo `json:"specs"` } + +// LoginDeviceResponse holds the details to complete a LoginDevice flow. +type LoginDeviceResponse struct { + // VerificationURI holds the URI that the user must navigate to + // when entering their "user-code" to consent to this authorisation + // request. + VerificationURI string `json:"verification-uri"` + // UserCode holds the one-time use user consent code. + UserCode string `json:"user-code"` + // DeviceLoginID contains the login id to be sent to GetDeviceAccessToken in + // order to begin a CLI based short-lived session. + DeviceLoginID string `json:"device-login-id"` +} + +// LoginDeviceAccessTokenRequest holds no parameters to initiate a device login. +type LoginDeviceAccessTokenRequest struct{} + +// LoginDeviceAccessTokenResponse TODO +type LoginDeviceAccessTokenResponse struct{} diff --git a/cmd/jimmctl/cmd/jimmsuite_test.go b/cmd/jimmctl/cmd/jimmsuite_test.go index f3540c245..c1683ee7b 100644 --- a/cmd/jimmctl/cmd/jimmsuite_test.go +++ b/cmd/jimmctl/cmd/jimmsuite_test.go @@ -84,10 +84,10 @@ func (s *jimmSuite) SetUpTest(c *gc.C) { JWTExpiryDuration: time.Minute, InsecureSecretStorage: true, OAuthAuthenticatorParams: service.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - AccessTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), }, } diff --git a/cmd/jimmsrv/main.go b/cmd/jimmsrv/main.go index f2fea7512..4b3c6831b 100644 --- a/cmd/jimmsrv/main.go +++ b/cmd/jimmsrv/main.go @@ -69,7 +69,7 @@ func start(ctx context.Context, s *service.Service) error { } } - accessTokenExpiryDuration := time.Duration(0) + sessionTokenExpiryDuration := time.Duration(0) durationString = os.Getenv("JIMM_ACCESS_TOKEN_EXPIRY_DURATION") if durationString != "" { expiry, err := time.ParseDuration(durationString) @@ -77,7 +77,7 @@ func start(ctx context.Context, s *service.Service) error { zapctx.Error(ctx, "failed to parse access token expiry duration", zap.Error(err)) return err } - accessTokenExpiryDuration = expiry + sessionTokenExpiryDuration = expiry } issuerURL := os.Getenv("JIMM_OAUTH_ISSUER_URL") @@ -145,10 +145,10 @@ func start(ctx context.Context, s *service.Service) error { InsecureSecretStorage: insecureSecretStorage, InsecureJwksLookup: insecureJwksLookup, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: issuerURL, - DeviceClientID: deviceClientID, - DeviceScopes: deviceScopesParsed, - AccessTokenExpiry: accessTokenExpiryDuration, + IssuerURL: issuerURL, + DeviceClientID: deviceClientID, + DeviceScopes: deviceScopesParsed, + SessionTokenExpiry: sessionTokenExpiryDuration, }, }) if err != nil { diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 59a11cd37..c63b1da8f 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -23,8 +23,8 @@ type AuthenticationService struct { // provider holds a OIDC provider wrapper for the OAuth2.0 /x/oauth package, // enabling UserInfo calls, wellknown retrieval and jwks verification. provider *oidc.Provider - // accessTokenExpiry holds the expiry time for JIMM minted access tokens (JWTs). - accessTokenExpiry time.Duration + // sessionTokenExpiry holds the expiry time for JIMM minted session tokens (JWTs). + sessionTokenExpiry time.Duration } // AuthenticationServiceParams holds the parameters to initialise @@ -39,8 +39,8 @@ type AuthenticationServiceParams struct { DeviceClientID string // DeviceScopes holds the scopes that you wish to retrieve. DeviceScopes []string - // AccessTokenExpiry holds the expiry time of minted JIMM access tokens (JWTs). - AccessTokenExpiry time.Duration + // SessionTokenExpiry holds the expiry time of minted JIMM session tokens (JWTs). + SessionTokenExpiry time.Duration } // NewAuthenticationService returns a new authentication service for handling @@ -61,7 +61,7 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP Endpoint: provider.Endpoint(), Scopes: params.DeviceScopes, }, - accessTokenExpiry: params.AccessTokenExpiry, + sessionTokenExpiry: params.SessionTokenExpiry, }, nil } @@ -148,14 +148,14 @@ func (as *AuthenticationService) Email(idToken *oidc.IDToken) (string, error) { return claims.Email, nil } -// MintAccessToken mints a session access token to be used when logging into JIMM +// MintSessionToken mints a session token to be used when logging into JIMM // via an access token. The token only contains the user's email for authentication. -func (as *AuthenticationService) MintAccessToken(email string, secretKey string) ([]byte, error) { +func (as *AuthenticationService) MintSessionToken(email string, secretKey string) ([]byte, error) { const op = errors.Op("auth.AuthenticationService.MintAccessToken") token, err := jwt.NewBuilder(). Subject(email). - Expiration(time.Now().Add(as.accessTokenExpiry)). + Expiration(time.Now().Add(as.sessionTokenExpiry)). Build() if err != nil { return nil, errors.E(op, err, "failed to build access token") diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index ac8d3ea0c..d99d5f7e4 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -121,15 +121,15 @@ func TestAccessTokens(t *testing.T) { ctx := context.Background() authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - AccessTokenExpiry: time.Hour, + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Hour, }) c.Assert(err, qt.IsNil) secretKey := "secret-key" - token, err := authSvc.MintAccessToken("jimm-test@canonical.com", secretKey) + token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) c.Assert(err, qt.IsNil) c.Assert(len(token) > 0, qt.IsTrue) @@ -144,15 +144,15 @@ func TestAccessTokenRejectsWrongSecretKey(t *testing.T) { ctx := context.Background() authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - AccessTokenExpiry: time.Hour, + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Hour, }) c.Assert(err, qt.IsNil) secretKey := "secret-key" - token, err := authSvc.MintAccessToken("jimm-test@canonical.com", secretKey) + token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) c.Assert(err, qt.IsNil) c.Assert(len(token) > 0, qt.IsTrue) @@ -168,15 +168,15 @@ func TestAccessTokenRejectsExpiredToken(t *testing.T) { noDuration := time.Duration(0) authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - AccessTokenExpiry: noDuration, + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: noDuration, }) c.Assert(err, qt.IsNil) secretKey := "secret-key" - token, err := authSvc.MintAccessToken("jimm-test@canonical.com", secretKey) + token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) c.Assert(err, qt.IsNil) c.Assert(len(token) > 0, qt.IsTrue) @@ -190,15 +190,15 @@ func TestAccessTokenValidatesEmail(t *testing.T) { ctx := context.Background() authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - AccessTokenExpiry: time.Hour, + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Hour, }) c.Assert(err, qt.IsNil) secretKey := "secret-key" - token, err := authSvc.MintAccessToken("", secretKey) + token, err := authSvc.MintSessionToken("", secretKey) c.Assert(err, qt.IsNil) c.Assert(len(token) > 0, qt.IsTrue) diff --git a/internal/jimmjwx/utils_test.go b/internal/jimmjwx/utils_test.go index ecc59081a..a80a3fbad 100644 --- a/internal/jimmjwx/utils_test.go +++ b/internal/jimmjwx/utils_test.go @@ -109,10 +109,10 @@ func setupService(ctx context.Context, c *qt.C) (*jimm.Service, *httptest.Server AuthModel: cofgaParams.AuthModelID, }, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - AccessTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), }, }) c.Assert(err, qt.IsNil) diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index 50078819a..ff1b1f14c 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -11,6 +11,7 @@ import ( jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v4" + "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/servermon" @@ -77,3 +78,26 @@ func (r *controllerRoot) Login(ctx context.Context, req jujuparams.LoginRequest) ServerVersion: srvVersion.String(), }, nil } + +// LoginDevice starts a device login flow (typically a CLI). It will return a verification URI +// and user code that the user is expected to enter into the verification URI link. +// +// Upon successful login, the user is then expected to retrieve an access token using +// GetDeviceAccessToken. +func (r *controllerRoot) LoginDevice(ctx context.Context) (params.LoginDeviceResponse, error) { + const op = errors.Op("jujuapi.LoginDevice") + response := params.LoginDeviceResponse{} + authSvc := r.jimm.OAuthAuthenticationService() + + deviceResponse, err := authSvc.Device(ctx) + if err != nil { + return response, errors.E(op, err) + } + + r.deviceOAuthResponse = deviceResponse + + response.VerificationURI = deviceResponse.VerificationURI + response.UserCode = deviceResponse.UserCode + + return response, nil +} diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index c615290e9..0f3ac621f 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -3,6 +3,14 @@ package jujuapi_test import ( + "fmt" + "io" + "net/http" + "net/http/cookiejar" + "net/url" + "regexp" + + "github.com/canonical/jimm/api/params" "github.com/juju/juju/api" jujuparams "github.com/juju/juju/rpc/params" gc "gopkg.in/check.v1" @@ -35,3 +43,104 @@ func (s *adminSuite) TestLoginToControllerWithInvalidMacaroon(c *gc.C) { }, "test") conn.Close() } + +// TestDeviceLogin takes a test user through the flow of logging into jimm +// via the correct facades. All are done in a single test to see the flow end-2-end. +// +// Within the test are clear comments explaining what is happening when and why. +// Please refer to these comments for further details. +func (s *adminSuite) TestDeviceLogin(c *gc.C) { + conn := s.open(c, &api.Info{ + SkipLogin: true, + }, "test") + defer conn.Close() + + // We create a http client to keep the same cookies across all requests + // using a simple jar. + jar, err := cookiejar.New(nil) + c.Assert(err, gc.IsNil) + + client := &http.Client{ + Jar: jar, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + fmt.Println("redirected to", req.URL) + return nil + }, + } + + // Step 1, initiate a device login and get the verification URI and usercode. + // Next, the user will send this code to the verification URI. + // + // To simplify the test, we're not going the browser route and instead + // are going to use the VerificationURIComplete which is equivalent to scanning + // a QR code. Both are ultimately the same. + // + // A normal verification URI looks like: http://localhost:8082/realms/jimm/device + // in which the user code is posted. + // + // A complete URI looks like: http://localhost:8082/realms/jimm/device?user_code=HOKO-OTRV + // where the user code is set as a part of the query string. + var resp params.LoginDeviceResponse + err = conn.APICall("Admin", 4, "", "LoginDevice", nil, &resp) + c.Assert(err, gc.IsNil) + c.Assert(resp.UserCode, gc.Not(gc.IsNil)) + c.Assert(resp.VerificationURI, gc.Equals, "http://localhost:8082/realms/jimm/device") + + // Step 2, complete the user side of the authentication by sending the + // user code to the verification URI using the "complete" method. + userResp, err := client.Get(resp.VerificationURI + "?user_code=" + resp.UserCode) + c.Assert(err, gc.IsNil) + body := userResp.Body + defer body.Close() + b, err := io.ReadAll(body) + c.Assert(err, gc.IsNil) + loginForm := string(b) + + // Step 2.1, handle the login form (see this func for more details) + handleLoginForm(c, loginForm, client) +} + +// handleLoginForm runs through the login process emulating the user typing in +// their username and password and then clicking consent, to complete +// the device login flow. +func handleLoginForm(c *gc.C, loginForm string, client *http.Client) { + // Step 2.2, now we'll be redirected to a sign-in page and must sign in. + re := regexp.MustCompile(`action="(.*?)" method=`) + match := re.FindStringSubmatch(loginForm) + loginFormUrl := match[1] + + // The username and password are hardcoded witih jimm-realm.json in our local + // keycloak configuration for the jimm realm. + v := url.Values{} + v.Add("username", "jimm-test") + v.Add("password", "password") + loginResp, err := client.PostForm(loginFormUrl, v) + c.Assert(err, gc.IsNil) + + loginRespBody := loginResp.Body + defer loginRespBody.Close() + + // Step 2.3, the user will now be redirected to a consent screen + // and is expected to click "yes". We simulate this by posting the form programatically. + loginRespB, err := io.ReadAll(loginRespBody) + c.Assert(err, gc.IsNil) + loginRespS := string(loginRespB) + + re = regexp.MustCompile(`action="(.*?)" method=`) + match = re.FindStringSubmatch(loginRespS) + consentFormUri := match[1] + + // We post the "yes" value to accept it. + v = url.Values{} + v.Add("accept", "Yes") + consentResp, err := client.PostForm("http://localhost:8082"+consentFormUri, v) + c.Assert(err, gc.IsNil) + defer consentResp.Body.Close() + + // Read the response to ensure it is OK and has been accepted. + b, err := io.ReadAll(consentResp.Body) + c.Assert(err, gc.IsNil) + + re = regexp.MustCompile(`Device Login Successful`) + c.Assert(re.MatchString(string(b)), gc.Equals, true) +} diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index 9bbf4e679..73ff0f4af 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -12,6 +12,7 @@ import ( "github.com/juju/names/v4" "github.com/juju/version" "github.com/rogpeppe/fastuuid" + "golang.org/x/oauth2" "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/db" @@ -30,6 +31,7 @@ type JIMM interface { AddHostedCloud(ctx context.Context, user *openfga.User, tag names.CloudTag, cloud jujuparams.Cloud, force bool) error AddModel(ctx context.Context, u *openfga.User, args *jimm.ModelCreateArgs) (_ *jujuparams.ModelInfo, err error) Authenticate(ctx context.Context, req *jujuparams.LoginRequest) (*openfga.User, error) + OAuthAuthenticationService() jimm.OAuthAuthenticator AuthorizationClient() *openfga.OFGAClient ChangeModelCredential(ctx context.Context, user *openfga.User, modelTag names.ModelTag, cloudCredentialTag names.CloudCredentialTag) error DB() *db.Database @@ -108,6 +110,11 @@ type controllerRoot struct { user *openfga.User controllerUUIDMasking bool generator *fastuuid.Generator + + // deviceOAuthResponse holds a device code flow response for this request, + // such that JIMM can retrieve the access and ID tokens via polling the Authentication + // Service's issuer via the /token endpoint. + deviceOAuthResponse *oauth2.DeviceAuthResponse } func newControllerRoot(j JIMM, p Params) *controllerRoot { @@ -125,6 +132,8 @@ func newControllerRoot(j JIMM, p Params) *controllerRoot { r.AddMethod("Admin", 1, "Login", rpc.Method(unsupportedLogin)) r.AddMethod("Admin", 2, "Login", rpc.Method(unsupportedLogin)) r.AddMethod("Admin", 3, "Login", rpc.Method(r.Login)) + r.AddMethod("Admin", 4, "Login", rpc.Method(r.Login)) + r.AddMethod("Admin", 4, "LoginDevice", rpc.Method(r.LoginDevice)) r.AddMethod("Pinger", 1, "Ping", rpc.Method(r.Ping)) return r } diff --git a/service.go b/service.go index d269f033b..e99f32542 100644 --- a/service.go +++ b/service.go @@ -75,9 +75,9 @@ type OAuthAuthenticatorParams struct { DeviceClientID string // DeviceScopes holds the scopes that you wish to retrieve. DeviceScopes []string - // AccessTokenExpiry holds the expiry duration for issued JWTs + // SessionTokenExpiry holds the expiry duration for issued JWTs // for user (CLI) to JIMM authentication. - AccessTokenExpiry time.Duration + SessionTokenExpiry time.Duration } // A Params structure contains the parameters required to initialise a new diff --git a/service_test.go b/service_test.go index 19f854f46..d4a0ed818 100644 --- a/service_test.go +++ b/service_test.go @@ -51,10 +51,10 @@ func TestDefaultService(t *testing.T) { OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), InsecureSecretStorage: true, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - AccessTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), }, }) c.Assert(err, qt.IsNil) @@ -75,10 +75,10 @@ func TestServiceStartsWithoutSecretStore(t *testing.T) { DSN: jimmtest.CreateEmptyDatabase(c), OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - AccessTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), }, }) c.Assert(err, qt.IsNil) @@ -97,10 +97,10 @@ func TestAuthenticator(t *testing.T) { OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), InsecureSecretStorage: true, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - AccessTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), }, } candid := startCandid(c, &p) @@ -166,10 +166,10 @@ func TestVault(t *testing.T) { VaultSecretFile: "./local/vault/approle.json", OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - AccessTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), }, } candid := startCandid(c, &p) @@ -235,10 +235,10 @@ func TestPostgresSecretStore(t *testing.T) { OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), InsecureSecretStorage: true, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - AccessTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), }, } _, err = jimm.NewService(context.Background(), p) @@ -257,10 +257,10 @@ func TestOpenFGA(t *testing.T) { OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), ControllerAdmins: []string{"alice", "eve"}, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - AccessTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), }, } candid := startCandid(c, &p) @@ -313,10 +313,10 @@ func TestPublicKey(t *testing.T) { PrivateKey: "c1VkV05+iWzCxMwMVcWbr0YJWQSEO62v+z3EQ2BhFMw=", PublicKey: "pC8MEk9MS9S8fhyRnOJ4qARTcTAwoM9L1nH/Yq0MwWU=", OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - AccessTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), }, } _ = startCandid(c, &p) @@ -401,10 +401,10 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { PrivateKey: "c1VkV05+iWzCxMwMVcWbr0YJWQSEO62v+z3EQ2BhFMw=", PublicKey: "pC8MEk9MS9S8fhyRnOJ4qARTcTAwoM9L1nH/Yq0MwWU=", OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - AccessTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + DeviceClientID: "jimm-device", + DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), }, } _ = startCandid(c, &p) From a5babdab2b0df83ec53e7e762bfd007d7ba00d52 Mon Sep 17 00:00:00 2001 From: Kian Parvin Date: Wed, 7 Feb 2024 12:16:51 +0200 Subject: [PATCH 055/126] Update go.mod --- go.mod | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 1758bc76c..ee3546836 100644 --- a/go.mod +++ b/go.mod @@ -53,6 +53,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/itchyny/gojq v0.12.12 github.com/juju/charm/v11 v11.0.2 + github.com/lestrrat-go/iter v1.0.2 github.com/lestrrat-go/jwx/v2 v2.0.11 github.com/oklog/ulid/v2 v2.1.0 github.com/stretchr/testify v1.8.4 @@ -241,7 +242,6 @@ require ( github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect - github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/lestrrat/go-jspointer v0.0.0-20160229021354-f4881e611bdb // indirect github.com/lestrrat/go-jsref v0.0.0-20160601013240-e452c7b5801d // indirect @@ -344,9 +344,9 @@ require ( go.uber.org/multierr v1.8.0 // indirect golang.org/x/crypto v0.16.0 // indirect golang.org/x/mod v0.13.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/term v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.14.0 // indirect google.golang.org/api v0.126.0 // indirect From 6a3751bd4fc572681010c2958c77b24b0e819d51 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 9 Feb 2024 10:34:54 +0200 Subject: [PATCH 056/126] CSS-7094 Make commands top level (#1156) * Add symlink * Add all cli commands * Cleaned up command docs * Change tabs to spaces * PR changes * PR comments --- cmd/jaas/cmd/addserviceaccount.go | 17 ++++++++------ cmd/jaas/cmd/grant.go | 8 +++---- cmd/jaas/cmd/listserviceaccountcredentials.go | 22 ++++++++++-------- cmd/jaas/cmd/updatecredentials.go | 4 ++-- cmd/jaas/main.go | 23 +++++++++++++++---- snaps/jaas/snapcraft.yaml | 16 +++++++++---- 6 files changed, 58 insertions(+), 32 deletions(-) diff --git a/cmd/jaas/cmd/addserviceaccount.go b/cmd/jaas/cmd/addserviceaccount.go index 35aace399..4479e2347 100644 --- a/cmd/jaas/cmd/addserviceaccount.go +++ b/cmd/jaas/cmd/addserviceaccount.go @@ -17,10 +17,11 @@ import ( var ( addServiceCommandDoc = ` -add command binds a service account to your user, giving you administrator access over the service account. - -Example: - juju service-account add +add-service-account binds a service account to your user, giving you administrator access over the service account. +Can only be run once per service account. +` + addServiceCommandExamples = ` + juju add-service-account ` ) @@ -46,9 +47,11 @@ type addServiceAccountCommand struct { // Info implements Command.Info. func (c *addServiceAccountCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "add", - Purpose: "Add service account", - Doc: addServiceCommandDoc, + Name: "add-service-account", + Purpose: "Add permission to manage a service account", + Args: "", + Examples: addServiceCommandExamples, + Doc: addServiceCommandDoc, }) } diff --git a/cmd/jaas/cmd/grant.go b/cmd/jaas/cmd/grant.go index c93d788c3..62eb86c35 100644 --- a/cmd/jaas/cmd/grant.go +++ b/cmd/jaas/cmd/grant.go @@ -18,10 +18,10 @@ import ( var ( grantCommandDoc = ` -grant command grants administrator access over a service account to the given groups/identities. +grant-service-account-access grants administrator access over a service account to the given groups/identities. ` grantCommandExamples = ` - juju service-accounts grant 00000000-0000-0000-0000-000000000000 user-foo group-bar + juju grant-service-account-access 00000000-0000-0000-0000-000000000000 user-foo group-bar ` ) @@ -49,9 +49,9 @@ type grantCommand struct { // Info implements Command.Info. func (c *grantCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "grant", + Name: "grant-service-account-access", Args: " (|) [(|) ...]", - Purpose: "Grants administrator access over a service account to the given groups/identities", + Purpose: "Grants administrator access over a service account", Examples: grantCommandExamples, Doc: grantCommandDoc, }) diff --git a/cmd/jaas/cmd/listserviceaccountcredentials.go b/cmd/jaas/cmd/listserviceaccountcredentials.go index 284d89722..03002f10a 100644 --- a/cmd/jaas/cmd/listserviceaccountcredentials.go +++ b/cmd/jaas/cmd/listserviceaccountcredentials.go @@ -25,16 +25,16 @@ import ( var ( listServiceCredentialsCommandDoc = ` -list-credentials command list the cloud credentials belonging to a service account. +list-credentials lists the cloud credentials belonging to a service account. This command only shows credentials uploaded to the controller that belong to the service account. +Client-side credentials should be managed via the juju credentials command. -Client credentials should be managed via juju credentials. - -Example: - juju service-account list-credentials - juju service-account list-credentials --show-secrets - juju service-account list-credentials --format yaml +` + listServiceAccountCredentialsExamples = ` + juju list-service-account-credentials + juju list-service-account-credentials --show-secrets + juju list-service-account-credentials --format yaml ` ) @@ -60,9 +60,11 @@ type listServiceAccountCredentialsCommand struct { func (c *listServiceAccountCredentialsCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "list-credentials", - Purpose: "List service account cloud credentials", - Doc: listServiceCredentialsCommandDoc, + Name: "list-service-account-credentials", + Purpose: "List service account cloud credentials", + Args: "", + Doc: listServiceCredentialsCommandDoc, + Examples: listServiceAccountCredentialsExamples, }) } diff --git a/cmd/jaas/cmd/updatecredentials.go b/cmd/jaas/cmd/updatecredentials.go index c979a9b85..7fdf98357 100644 --- a/cmd/jaas/cmd/updatecredentials.go +++ b/cmd/jaas/cmd/updatecredentials.go @@ -27,7 +27,7 @@ This will add the credentials to JAAS if they were not found. ` updateCredentialsCommandExamples = ` - juju service-account update-credentials 00000000-0000-0000-0000-000000000000 aws credential-name + juju update-service-account-credentials update-credentials 00000000-0000-0000-0000-000000000000 aws credential-name ` ) @@ -56,7 +56,7 @@ type updateCredentialsCommand struct { // Info implements Command.Info. func (c *updateCredentialsCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "update-credentials", + Name: "update-service-account-credentials", Purpose: "Update service account cloud credentials", Args: " ", Doc: updateCredentialsCommandDoc, diff --git a/cmd/jaas/main.go b/cmd/jaas/main.go index 96eecb632..217f24e37 100644 --- a/cmd/jaas/main.go +++ b/cmd/jaas/main.go @@ -5,13 +5,14 @@ package main import ( "fmt" "os" + "strings" "github.com/canonical/jimm/cmd/jaas/cmd" jujucmd "github.com/juju/cmd/v3" ) var jaasDoc = ` -juju jaas enables users to use JAAS commands from within Juju. +jaas enables users to use JAAS commands from within the Juju CLI. JAAS enables enterprise functionality on top of Juju to provide functionality like OIDC login, control over many controllers, @@ -31,6 +32,11 @@ func NewSuperCommand() *jujucmd.SuperCommand { return serviceAccountCmd } +const ( + jujuPrefix = "juju-" + jaasCommand = "juju-jaas" +) + func main() { ctx, err := jujucmd.DefaultContext() if err != nil { @@ -38,7 +44,16 @@ func main() { os.Exit(2) } superCmd := NewSuperCommand() - args := os.Args - - os.Exit(jujucmd.Main(superCmd, ctx, args[1:])) + var args []string + // The following if condition handles cases where the juju binary calls jaas as a plugin. + // Symlinks of the form juju- are created to make all jaas commands appear as top + // level commands to the Juju CLI and then we strip the juju- prefix to obtain the desired function. + if strings.HasPrefix(os.Args[0], jujuPrefix) && os.Args[0] != jaasCommand { + args = make([]string, len(os.Args)) + copy(args[1:], os.Args[1:]) + args[0] = strings.TrimPrefix(os.Args[0], "juju-") + } else { + args = os.Args[1:] + } + os.Exit(jujucmd.Main(superCmd, ctx, args)) } diff --git a/snaps/jaas/snapcraft.yaml b/snaps/jaas/snapcraft.yaml index 3c596ea60..9189b7c33 100644 --- a/snaps/jaas/snapcraft.yaml +++ b/snaps/jaas/snapcraft.yaml @@ -17,7 +17,7 @@ slots: # The app has no plugs as it is intended to be invoked by the Juju CLI Snap. apps: jaas: - command: bin/juju-jaas + command: bin/jaas parts: jaas: @@ -25,9 +25,15 @@ parts: source: ./ source-type: git prime: - - bin/juju-jaas + - bin/jaas override-build: | set -e - CGO_ENABLED=0 go build -o juju-jaas github.com/canonical/jimm/cmd/jaas - mkdir -p $GOBIN - cp ./juju-jaas $GOBIN/juju-jaas + CGO_ENABLED=0 go install github.com/canonical/jimm/cmd/jaas + override-prime: | + snapcraftctl prime + # Add all CLI commands below to make them appear top-level to Juju. + ln -sf jaas bin/juju-jaas + ln -sf jaas bin/juju-add-service-account + ln -sf jaas bin/juju-list-service-account-credentials + ln -sf jaas bin/juju-update-service-account-credentials + ln -sf jaas bin/juju-grant-service-account-access From 1d16690c730ccd763b1263323f38bf73642aaa50 Mon Sep 17 00:00:00 2001 From: Kian Parvin Date: Fri, 9 Feb 2024 10:54:02 +0200 Subject: [PATCH 057/126] Update jimm_mock.go --- internal/jimmtest/jimm_mock.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/jimmtest/jimm_mock.go b/internal/jimmtest/jimm_mock.go index e5d5b71c4..32ef6e217 100644 --- a/internal/jimmtest/jimm_mock.go +++ b/internal/jimmtest/jimm_mock.go @@ -77,6 +77,7 @@ type JIMM struct { ModelInfo_ func(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelInfo, error) ModelStatus_ func(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelStatus, error) Offer_ func(ctx context.Context, user *openfga.User, offer jimm.AddApplicationOfferParams) error + OAuthAuthenticationService_ func() jimm.OAuthAuthenticator PubSubHub_ func() *pubsub.Hub PurgeLogs_ func(ctx context.Context, user *openfga.User, before time.Time) (int64, error) QueryModelsJq_ func(ctx context.Context, models []dbmodel.Model, jqQuery string) (params.CrossModelQueryResponse, error) @@ -395,6 +396,12 @@ func (j *JIMM) Offer(ctx context.Context, user *openfga.User, offer jimm.AddAppl } return j.Offer_(ctx, user, offer) } +func (j *JIMM) OAuthAuthenticationService() jimm.OAuthAuthenticator { + if j.OAuthAuthenticationService_ == nil { + panic("not implemented") + } + return j.OAuthAuthenticationService_() +} func (j *JIMM) PubSubHub() *pubsub.Hub { if j.PubSubHub_ == nil { panic("not implemented") From 2f4c56c19bfc13c7810f45bb3711e4c19e3148fc Mon Sep 17 00:00:00 2001 From: Kian Parvin Date: Fri, 9 Feb 2024 17:17:14 +0200 Subject: [PATCH 058/126] Add error for logs --- internal/jimmtest/keycloak.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/jimmtest/keycloak.go b/internal/jimmtest/keycloak.go index 235486a38..dcb912c30 100644 --- a/internal/jimmtest/keycloak.go +++ b/internal/jimmtest/keycloak.go @@ -4,6 +4,7 @@ package jimmtest import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -13,6 +14,8 @@ import ( "github.com/canonical/jimm/internal/errors" "github.com/google/uuid" + "github.com/juju/zaputil/zapctx" + "go.uber.org/zap" ) // These constants are based on the `docker-compose.yaml` and `local/keycloak/jimm-realm.json` content. @@ -42,6 +45,7 @@ func CreateRandomKeycloakUser() (*KeycloakUser, error) { adminCLIToken, err := getAdminCLIAccessToken() if err != nil { + zapctx.Error(context.Background(), "failed to authenticate admin CLI user", zap.Error(err)) return nil, errors.E(err, "failed to authenticate admin CLI user") } From 83a2ad68eb49fe8c893ef68fbbfd00eeb9da59bc Mon Sep 17 00:00:00 2001 From: Kian Parvin Date: Mon, 12 Feb 2024 09:44:49 +0200 Subject: [PATCH 059/126] Update Keycloak health check --- docker-compose.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 2a1d149c4..5dfc9c463 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -248,7 +248,7 @@ services: ports: - "8082:8082" healthcheck: - test: [ "CMD", "curl", "http://localhost:8082/realms/jimm/.well-known/openid-configuration" ] + test: [ "CMD", "curl", "http://localhost:8082/health/ready" ] interval: 5s - timeout: 5s + timeout: 10s retries: 30 From a2811b05bf80e4ed2cdc025e1db69c3d6ed7e8b6 Mon Sep 17 00:00:00 2001 From: Kian Parvin Date: Mon, 12 Feb 2024 09:44:49 +0200 Subject: [PATCH 060/126] Update Keycloak health check --- docker-compose.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 6cba6cc50..5978d8c1a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -247,7 +247,7 @@ services: ports: - "8082:8082" healthcheck: - test: [ "CMD", "curl", "http://localhost:8082/realms/jimm/.well-known/openid-configuration" ] + test: [ "CMD", "curl", "http://localhost:8082/health/ready" ] interval: 5s - timeout: 5s + timeout: 10s retries: 30 From 40a9e68e9e8c12483cab50934a4f5a4d95318035 Mon Sep 17 00:00:00 2001 From: Kian Parvin Date: Mon, 12 Feb 2024 15:53:54 +0200 Subject: [PATCH 061/126] Ensure the changes from v3 have user renamed to identity After merging v3 into feature-oidc, there were several new files that needed renaming from `user` to `identity`. Additionally, the grantAccess handler in jujuapi/serviceaccount.go needed refactoring. --- internal/jimm/access.go | 8 ++-- internal/jimm/access_test.go | 26 +++++----- internal/jimm/service_account.go | 20 +++++++- internal/jimm/service_account_test.go | 61 +++++++++++++++++------- internal/jimm/user.go | 6 +-- internal/jimmtest/jimm_mock.go | 55 +++++++++++++++++++-- internal/jujuapi/controllerroot.go | 3 +- internal/jujuapi/service_account.go | 20 +------- internal/jujuapi/service_account_test.go | 45 +---------------- 9 files changed, 139 insertions(+), 105 deletions(-) diff --git a/internal/jimm/access.go b/internal/jimm/access.go index 7c9c1f2f6..0ca885462 100644 --- a/internal/jimm/access.go +++ b/internal/jimm/access.go @@ -427,7 +427,7 @@ func (j *JIMM) ToJAASTag(ctx context.Context, tag *ofganames.Tag) (string, error if err != nil { return "", errors.E(err, "failed to fetch model information") } - modelString := names.ModelTagKind + "-" + model.Controller.Name + ":" + model.OwnerUsername + "/" + model.Name + modelString := names.ModelTagKind + "-" + model.Controller.Name + ":" + model.OwnerIdentityName + "/" + model.Name if tag.Relation.String() != "" { modelString = modelString + "#" + tag.Relation.String() } @@ -440,7 +440,7 @@ func (j *JIMM) ToJAASTag(ctx context.Context, tag *ofganames.Tag) (string, error if err != nil { return "", errors.E(err, "failed to fetch application offer information") } - aoString := names.ApplicationOfferTagKind + "-" + ao.Model.Controller.Name + ":" + ao.Model.OwnerUsername + "/" + ao.Model.Name + "." + ao.Name + aoString := names.ApplicationOfferTagKind + "-" + ao.Model.Controller.Name + ":" + ao.Model.OwnerIdentityName + "/" + ao.Model.Name + "." + ao.Name if tag.Relation.String() != "" { aoString = aoString + "#" + tag.Relation.String() } @@ -534,7 +534,7 @@ func resolveTag(jimmUUID string, db *db.Database, tag string) (*ofganames.Tag, e } err := db.GetGroup(ctx, entry) if err != nil { - return nil, errors.E("group not found") + return nil, errors.E(fmt.Sprintf("group %s not found", trailer)) } return ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(strconv.FormatUint(uint64(entry.ID), 10)), relation), nil @@ -580,7 +580,7 @@ func resolveTag(jimmUUID string, db *db.Database, tag string) (*ofganames.Tag, e return nil, errors.E("controller not found") } model.ControllerID = controller.ID - model.OwnerUsername = userName + model.OwnerIdentityName = userName model.Name = modelName } diff --git a/internal/jimm/access_test.go b/internal/jimm/access_test.go index 1afb37ee0..95104dc06 100644 --- a/internal/jimm/access_test.go +++ b/internal/jimm/access_test.go @@ -479,7 +479,7 @@ func TestParseTag(t *testing.T) { user, _, controller, model, _, _, _ := createTestControllerEnvironment(ctx, c, j.Database) - jimmTag := "model-" + controller.Name + ":" + user.Username + "/" + model.Name + "#administrator" + jimmTag := "model-" + controller.Name + ":" + user.Name + "/" + model.Name + "#administrator" // JIMM tag syntax for models tag, err := j.ParseTag(ctx, jimmTag) @@ -545,7 +545,7 @@ func TestResolveTupleObjectMapsApplicationOffersUUIDs(t *testing.T) { user, _, controller, model, offer, _, _ := createTestControllerEnvironment(ctx, c, j.Database) - jimmTag := "applicationoffer-" + controller.Name + ":" + user.Username + "/" + model.Name + "." + offer.Name + "#administrator" + jimmTag := "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name + "#administrator" jujuTag, err := jimm.ResolveTag(j.UUID, &j.Database, jimmTag) c.Assert(err, qt.IsNil) @@ -573,7 +573,7 @@ func TestResolveTupleObjectMapsModelUUIDs(t *testing.T) { user, _, controller, model, _, _, _ := createTestControllerEnvironment(ctx, c, j.Database) - jimmTag := "model-" + controller.Name + ":" + user.Username + "/" + model.Name + "#administrator" + jimmTag := "model-" + controller.Name + ":" + user.Name + "/" + model.Name + "#administrator" tag, err := jimm.ResolveTag(j.UUID, &j.Database, jimmTag) c.Assert(err, qt.IsNil) @@ -709,7 +709,7 @@ func TestResolveTupleObjectHandlesErrors(t *testing.T) { // Resolves bad groups where they do not exist { input: "group-myspecialpokemon-his-name-is-youguessedit-diglett", - want: "group not found", + want: "group myspecialpokemon-his-name-is-youguessedit-diglett not found", }, // Resolves bad controllers where they do not exist { @@ -756,7 +756,7 @@ func TestResolveTupleObjectHandlesErrors(t *testing.T) { // TODO(ale8k): Make this an implicit thing on the JIMM suite per test & refactor the current state. // and make the suite argument an interface of the required calls we use here. func createTestControllerEnvironment(ctx context.Context, c *qt.C, db db.Database) ( - dbmodel.User, + dbmodel.Identity, dbmodel.GroupEntry, dbmodel.Controller, dbmodel.Model, @@ -770,8 +770,8 @@ func createTestControllerEnvironment(ctx context.Context, c *qt.C, db db.Databas err = db.GetGroup(ctx, &group) c.Assert(err, qt.IsNil) - u := dbmodel.User{ - Username: petname.Generate(2, "-") + "@external", + u := dbmodel.Identity{ + Name: petname.Generate(2, "-") + "@external", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -798,10 +798,10 @@ func createTestControllerEnvironment(ctx context.Context, c *qt.C, db db.Databas c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ - Name: petname.Generate(2, "-"), - CloudName: cloud.Name, - OwnerUsername: u.Username, - AuthType: "empty", + Name: petname.Generate(2, "-"), + CloudName: cloud.Name, + OwnerIdentityName: u.Name, + AuthType: "empty", } err = db.SetCloudCredential(ctx, &cred) c.Assert(err, qt.IsNil) @@ -812,7 +812,7 @@ func createTestControllerEnvironment(ctx context.Context, c *qt.C, db db.Databas String: id.String(), Valid: true, }, - OwnerUsername: u.Username, + OwnerIdentityName: u.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -830,7 +830,7 @@ func createTestControllerEnvironment(ctx context.Context, c *qt.C, db db.Databas c.Assert(err, qt.IsNil) offerName := petname.Generate(2, "-") - offerURL, err := crossmodel.ParseOfferURL(controller.Name + ":" + u.Username + "/" + model.Name + "." + offerName) + offerURL, err := crossmodel.ParseOfferURL(controller.Name + ":" + u.Name + "/" + model.Name + "." + offerName) c.Assert(err, qt.IsNil) offer := dbmodel.ApplicationOffer{ diff --git a/internal/jimm/service_account.go b/internal/jimm/service_account.go index 4f3ec4463..a1f665751 100644 --- a/internal/jimm/service_account.go +++ b/internal/jimm/service_account.go @@ -58,14 +58,30 @@ func (j *JIMM) AddServiceAccount(ctx context.Context, u *openfga.User, clientId // GrantServiceAccountAccess creates an administrator relation between the tags provided // and the service account. The provided tags must be users or groups (with the member relation) // otherwise OpenFGA will report an error. -func (j *JIMM) GrantServiceAccountAccess(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error { +func (j *JIMM) GrantServiceAccountAccess(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, entities []string) error { op := errors.Op("jimm.GrantServiceAccountAccess") + tags := make([]*ofganames.Tag, 0, len(entities)) + // Validate tags + for _, val := range entities { + tag, err := j.ParseTag(ctx, val) + if err != nil { + return errors.E(op, err) + } + if tag.Kind != openfga.UserType && tag.Kind != openfga.GroupType { + return errors.E(op, "invalid entity - not user or group") + } + if tag.Kind == openfga.GroupType { + tag.Relation = ofganames.MemberRelation + } + tags = append(tags, tag) + } tuples := make([]openfga.Tuple, 0, len(tags)) + svcAccEntity := ofganames.ConvertTag(svcAccTag) for _, tag := range tags { tuple := openfga.Tuple{ Object: tag, Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(svcAccTag), + Target: svcAccEntity, } tuples = append(tuples, tuple) } diff --git a/internal/jimm/service_account_test.go b/internal/jimm/service_account_test.go index 1ffb238fe..00ffa8b6d 100644 --- a/internal/jimm/service_account_test.go +++ b/internal/jimm/service_account_test.go @@ -16,7 +16,6 @@ import ( "github.com/canonical/jimm/internal/openfga" ofganames "github.com/canonical/jimm/internal/openfga/names" jimmnames "github.com/canonical/jimm/pkg/names" - "github.com/canonical/ofga" ) func TestAddServiceAccount(t *testing.T) { @@ -59,31 +58,50 @@ func TestGrantServiceAccountAccess(t *testing.T) { about string grantServiceAccountAccess func(ctx context.Context, user *openfga.User, tags []string) error clientID string - tags []*ofganames.Tag + tags []string username string + addGroups []string expectedError string }{{ about: "Valid request", grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, tags []string) error { return nil }, - tags: []*ofganames.Tag{ - &ofga.Entity{ - Kind: "user", - ID: "alice", - }, - &ofga.Entity{ - Kind: "user", - ID: "bob", - }, - &ofga.Entity{ - Kind: "group", - ID: "1", - Relation: "member", - }, + addGroups: []string{"1"}, + tags: []string{ + "user-alice", + "user-bob", + "group-1#member", }, clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", username: "alice", + }, { + about: "Group that doesn't exist", + grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, tags []string) error { + return nil + }, + tags: []string{ + "user-alice", + "user-bob", + // This group doesn't exist. + "group-bar", + }, + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + username: "alice", + expectedError: "group bar not found", + }, { + about: "Invalid tags", + grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, tags []string) error { + return nil + }, + tags: []string{ + "user-alice", + "user-bob", + "controller-jimm", + }, + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + username: "alice", + expectedError: "invalid entity - not user or group", }} for _, test := range tests { @@ -103,14 +121,23 @@ func TestGrantServiceAccountAccess(t *testing.T) { var u dbmodel.Identity u.SetTag(names.NewUserTag(test.clientID)) svcAccountIdentity := openfga.NewUser(&u, ofgaClient) + svcAccountIdentity.JimmAdmin = true + if len(test.addGroups) > 0 { + for _, name := range test.addGroups { + err := jimm.AddGroup(context.Background(), svcAccountIdentity, name) + c.Assert(err, qt.IsNil) + } + } svcAccountTag := jimmnames.NewServiceAccountTag(test.clientID) err = jimm.GrantServiceAccountAccess(context.Background(), svcAccountIdentity, svcAccountTag, test.tags) if test.expectedError == "" { c.Assert(err, qt.IsNil) for _, tag := range test.tags { + parsedTag, err := jimm.ParseTag(context.Background(), tag) + c.Assert(err, qt.IsNil) tuple := openfga.Tuple{ - Object: tag, + Object: parsedTag, Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(test.clientID)), } diff --git a/internal/jimm/user.go b/internal/jimm/user.go index 8e8b7f1c9..f373e893d 100644 --- a/internal/jimm/user.go +++ b/internal/jimm/user.go @@ -65,10 +65,10 @@ func (j *JIMM) Authenticate(ctx context.Context, req *jujuparams.LoginRequest) ( func (j *JIMM) GetUser(ctx context.Context, username string) (*openfga.User, error) { const op = errors.Op("jimm.GetUser") - user := dbmodel.User{ - Username: username, + user := dbmodel.Identity{ + Name: username, } - if err := j.Database.GetUser(ctx, &user); err != nil { + if err := j.Database.GetIdentity(ctx, &user); err != nil { return nil, err } u := openfga.NewUser(&user, j.OpenFGAClient) diff --git a/internal/jimmtest/jimm_mock.go b/internal/jimmtest/jimm_mock.go index 32ef6e217..be96d6359 100644 --- a/internal/jimmtest/jimm_mock.go +++ b/internal/jimmtest/jimm_mock.go @@ -31,6 +31,7 @@ type JIMM struct { AddAuditLogEntry_ func(ale *dbmodel.AuditLogEntry) AddCloudToController_ func(ctx context.Context, user *openfga.User, controllerName string, tag names.CloudTag, cloud jujuparams.Cloud, force bool) error AddController_ func(ctx context.Context, u *openfga.User, ctl *dbmodel.Controller) error + AddGroup_ func(ctx context.Context, user *openfga.User, name string) error AddHostedCloud_ func(ctx context.Context, user *openfga.User, tag names.CloudTag, cloud jujuparams.Cloud, force bool) error AddModel_ func(ctx context.Context, u *openfga.User, args *jimm.ModelCreateArgs) (*jujuparams.ModelInfo, error) AddServiceAccount_ func(ctx context.Context, u *openfga.User, clientId string) error @@ -59,6 +60,7 @@ type JIMM struct { GetCloudCredentialAttributes_ func(ctx context.Context, u *openfga.User, cred *dbmodel.CloudCredential, hidden bool) (attrs map[string]string, redacted []string, err error) GetControllerConfig_ func(ctx context.Context, u *dbmodel.Identity) (*dbmodel.ControllerConfig, error) GetJimmControllerAccess_ func(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error) + GetUser_ func(ctx context.Context, username string) (*openfga.User, error) GetUserCloudAccess_ func(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error) GetUserControllerAccess_ func(ctx context.Context, user *openfga.User, controller names.ControllerTag) (string, error) GetUserModelAccess_ func(ctx context.Context, user *openfga.User, model names.ModelTag) (string, error) @@ -66,24 +68,28 @@ type JIMM struct { GrantCloudAccess_ func(ctx context.Context, user *openfga.User, ct names.CloudTag, ut names.UserTag, access string) error GrantModelAccess_ func(ctx context.Context, user *openfga.User, mt names.ModelTag, ut names.UserTag, access jujuparams.UserAccessPermission) error GrantOfferAccess_ func(ctx context.Context, u *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) error - GrantServiceAccountAccess_ func(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error + GrantServiceAccountAccess_ func(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, entities []string) error ImportModel_ func(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error IdentityModelDefaults_ func(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) InitiateMigration_ func(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec, targetControllerID uint) (jujuparams.InitiateMigrationResult, error) InitiateInternalMigration_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetController string) (jujuparams.InitiateMigrationResult, error) ListApplicationOffers_ func(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) ListControllers_ func(ctx context.Context, user *openfga.User) ([]dbmodel.Controller, error) + ListGroups_ func(ctx context.Context, user *openfga.User) ([]dbmodel.GroupEntry, error) ModelDefaultsForCloud_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) ModelInfo_ func(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelInfo, error) ModelStatus_ func(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelStatus, error) Offer_ func(ctx context.Context, user *openfga.User, offer jimm.AddApplicationOfferParams) error OAuthAuthenticationService_ func() jimm.OAuthAuthenticator + ParseTag_ func(ctx context.Context, key string) (*ofganames.Tag, error) PubSubHub_ func() *pubsub.Hub PurgeLogs_ func(ctx context.Context, user *openfga.User, before time.Time) (int64, error) QueryModelsJq_ func(ctx context.Context, models []dbmodel.Model, jqQuery string) (params.CrossModelQueryResponse, error) RemoveCloud_ func(ctx context.Context, u *openfga.User, ct names.CloudTag) error RemoveCloudFromController_ func(ctx context.Context, u *openfga.User, controllerName string, ct names.CloudTag) error RemoveController_ func(ctx context.Context, user *openfga.User, controllerName string, force bool) error + RemoveGroup_ func(ctx context.Context, user *openfga.User, name string) error + RenameGroup_ func(ctx context.Context, user *openfga.User, oldName, newName string) error ResourceTag_ func() names.ControllerTag RevokeAuditLogAccess_ func(ctx context.Context, user *openfga.User, targetUserTag names.UserTag) error RevokeCloudAccess_ func(ctx context.Context, user *openfga.User, ct names.CloudTag, ut names.UserTag, access string) error @@ -94,6 +100,7 @@ type JIMM struct { SetControllerDeprecated_ func(ctx context.Context, user *openfga.User, controllerName string, deprecated bool) error SetModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, configs map[string]interface{}) error SetIdentityModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error + ToJAASTag_ func(ctx context.Context, tag *ofganames.Tag) (string, error) UnsetModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error UpdateApplicationOffer_ func(ctx context.Context, controller *dbmodel.Controller, offerUUID string, removed bool) error UpdateCloud_ func(ctx context.Context, u *openfga.User, ct names.CloudTag, cloud jujuparams.Cloud) error @@ -122,6 +129,12 @@ func (j *JIMM) AddController(ctx context.Context, u *openfga.User, ctl *dbmodel. } return j.AddController_(ctx, u, ctl) } +func (j *JIMM) AddGroup(ctx context.Context, u *openfga.User, name string) error { + if j.AddGroup == nil { + return errors.E(errors.CodeNotImplemented) + } + return j.AddGroup_(ctx, u, name) +} func (j *JIMM) AddHostedCloud(ctx context.Context, user *openfga.User, tag names.CloudTag, cloud jujuparams.Cloud, force bool) error { if j.AddHostedCloud_ == nil { return errors.E(errors.CodeNotImplemented) @@ -292,6 +305,12 @@ func (j *JIMM) GetJimmControllerAccess(ctx context.Context, user *openfga.User, } return j.GetJimmControllerAccess_(ctx, user, tag) } +func (j *JIMM) GetUser(ctx context.Context, username string) (*openfga.User, error) { + if j.GetUser_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.GetUser(ctx, username) +} func (j *JIMM) GetUserCloudAccess(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error) { if j.GetUserCloudAccess_ == nil { return "", errors.E(errors.CodeNotImplemented) @@ -335,11 +354,11 @@ func (j *JIMM) GrantOfferAccess(ctx context.Context, u *openfga.User, offerURL s return j.GrantOfferAccess_(ctx, u, offerURL, ut, access) } -func (j *JIMM) GrantServiceAccountAccess(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error { +func (j *JIMM) GrantServiceAccountAccess(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, entities []string) error { if j.GrantServiceAccountAccess_ == nil { return errors.E(errors.CodeNotImplemented) } - return j.GrantServiceAccountAccess_(ctx, u, svcAccTag, tags) + return j.GrantServiceAccountAccess_(ctx, u, svcAccTag, entities) } func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error { @@ -372,6 +391,12 @@ func (j *JIMM) ListControllers(ctx context.Context, user *openfga.User) ([]dbmod } return j.ListControllers_(ctx, user) } +func (j *JIMM) ListGroups(ctx context.Context, user *openfga.User) ([]dbmodel.GroupEntry, error) { + if j.ListGroups_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.ListGroups_(ctx, user) +} func (j *JIMM) ModelDefaultsForCloud(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) { if j.ModelDefaultsForCloud_ == nil { return jujuparams.ModelDefaultsResult{}, errors.E(errors.CodeNotImplemented) @@ -402,6 +427,12 @@ func (j *JIMM) OAuthAuthenticationService() jimm.OAuthAuthenticator { } return j.OAuthAuthenticationService_() } +func (j *JIMM) ParseTag(ctx context.Context, key string) (*ofganames.Tag, error) { + if j.ParseTag_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.ParseTag_(ctx, key) +} func (j *JIMM) PubSubHub() *pubsub.Hub { if j.PubSubHub_ == nil { panic("not implemented") @@ -438,6 +469,18 @@ func (j *JIMM) RemoveController(ctx context.Context, user *openfga.User, control } return j.RemoveController_(ctx, user, controllerName, force) } +func (j *JIMM) RemoveGroup(ctx context.Context, user *openfga.User, name string) error { + if j.RemoveGroup_ == nil { + return errors.E(errors.CodeNotImplemented) + } + return j.RemoveGroup_(ctx, user, name) +} +func (j *JIMM) RenameGroup(ctx context.Context, user *openfga.User, oldName, newName string) error { + if j.RenameGroup_ == nil { + return errors.E(errors.CodeNotImplemented) + } + return j.RenameGroup_(ctx, user, oldName, newName) +} func (j *JIMM) ResourceTag() names.ControllerTag { if j.ResourceTag_ == nil { return names.NewControllerTag(uuid.NewString()) @@ -498,6 +541,12 @@ func (j *JIMM) SetIdentityModelDefaults(ctx context.Context, user *dbmodel.Ident } return j.SetIdentityModelDefaults_(ctx, user, configs) } +func (j *JIMM) ToJAASTag(ctx context.Context, tag *ofganames.Tag) (string, error) { + if j.ToJAASTag_ == nil { + return "", errors.E(errors.CodeNotImplemented) + } + return j.ToJAASTag_(ctx, tag) +} func (j *JIMM) UnsetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error { if j.UnsetModelDefaults_ == nil { return errors.E(errors.CodeNotImplemented) diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index d6e1063f8..8390d4875 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -67,7 +67,7 @@ type JIMM interface { GrantCloudAccess(ctx context.Context, user *openfga.User, ct names.CloudTag, ut names.UserTag, access string) error GrantModelAccess(ctx context.Context, user *openfga.User, mt names.ModelTag, ut names.UserTag, access jujuparams.UserAccessPermission) error GrantOfferAccess(ctx context.Context, u *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) error - GrantServiceAccountAccess(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error + GrantServiceAccountAccess(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []string) error IdentityModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) ImportModel(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error InitiateInternalMigration(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetController string) (jujuparams.InitiateMigrationResult, error) @@ -96,7 +96,6 @@ type JIMM interface { SetControllerConfig(ctx context.Context, u *openfga.User, args jujuparams.ControllerConfigSet) error SetControllerDeprecated(ctx context.Context, user *openfga.User, controllerName string, deprecated bool) error SetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, configs map[string]interface{}) error - SetUserModelDefaults(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error ToJAASTag(ctx context.Context, tag *ofganames.Tag) (string, error) UnsetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error UpdateApplicationOffer(ctx context.Context, controller *dbmodel.Controller, offerUUID string, removed bool) error diff --git a/internal/jujuapi/service_account.go b/internal/jujuapi/service_account.go index ac3419b78..9527a1dbb 100644 --- a/internal/jujuapi/service_account.go +++ b/internal/jujuapi/service_account.go @@ -13,7 +13,6 @@ import ( "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/openfga" - ofganames "github.com/canonical/jimm/internal/openfga/names" jimmnames "github.com/canonical/jimm/pkg/names" ) @@ -111,26 +110,11 @@ func (r *controllerRoot) ListServiceAccountCredentials(ctx context.Context, req func (r *controllerRoot) GrantServiceAccountAccess(ctx context.Context, req apiparams.GrantServiceAccountAccess) error { const op = errors.Op("jujuapi.GrantServiceAccountAccess") - targetUser, err := r.getServiceAccount(ctx, req.ClientID) + _, err := r.getServiceAccount(ctx, req.ClientID) if err != nil { return errors.E(op, err) } - tags := make([]*ofganames.Tag, 0, len(req.Entities)) - // Validate tags - for _, val := range req.Entities { - tag, err := parseTag(ctx, r.jimm.ResourceTag().Id(), r.jimm.DB(), val) - if err != nil { - return errors.E(op, err) - } - if tag.Kind != openfga.UserType && tag.Kind != openfga.GroupType { - return errors.E(op, "invalid entity - not user or group") - } - if tag.Kind == openfga.GroupType { - tag.Relation = ofganames.MemberRelation - } - tags = append(tags, tag) - } svcAccTag := jimmnames.NewServiceAccountTag(req.ClientID) - return r.jimm.GrantServiceAccountAccess(ctx, targetUser, svcAccTag, tags) + return r.jimm.GrantServiceAccountAccess(ctx, r.user, svcAccTag, req.Entities) } diff --git a/internal/jujuapi/service_account_test.go b/internal/jujuapi/service_account_test.go index 383c97214..9f1936930 100644 --- a/internal/jujuapi/service_account_test.go +++ b/internal/jujuapi/service_account_test.go @@ -333,7 +333,7 @@ func TestGrantServiceAccountAccess(t *testing.T) { tests := []struct { about string - grantServiceAccountAccess func(ctx context.Context, user *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error + grantServiceAccountAccess func(ctx context.Context, user *openfga.User, svcAccTag jimmnames.ServiceAccountTag, entities []string) error params params.GrantServiceAccountAccess tags []string username string @@ -341,7 +341,7 @@ func TestGrantServiceAccountAccess(t *testing.T) { expectedError string }{{ about: "Valid request", - grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error { + grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, svcAccTag jimmnames.ServiceAccountTag, entities []string) error { return nil }, params: params.GrantServiceAccountAccess{ @@ -357,47 +357,6 @@ func TestGrantServiceAccountAccess(t *testing.T) { Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), }}, - }, { - about: "Group that doesn't exist", - grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error { - return nil - }, - params: params.GrantServiceAccountAccess{ - Entities: []string{ - "user-alice", - "user-bob", - // This group doesn't exist. - "group-bar", - }, - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", - }, - username: "alice", - addTuples: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice")), - Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), - }}, - expectedError: "group bar not found", - }, { - about: "Invalid tags", - grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []*ofganames.Tag) error { - return nil - }, - params: params.GrantServiceAccountAccess{ - Entities: []string{ - "user-alice", - "user-bob", - "controller-jimm", - }, - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", - }, - username: "alice", - addTuples: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice")), - Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), - }}, - expectedError: "invalid entity - not user or group", }} for _, test := range tests { From 9157dbf16573e21d93c0f2494c79b9fd6f953806 Mon Sep 17 00:00:00 2001 From: Kian Parvin Date: Mon, 12 Feb 2024 16:32:28 +0200 Subject: [PATCH 062/126] Fix mock AddGroup function --- internal/jimmtest/jimm_mock.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/jimmtest/jimm_mock.go b/internal/jimmtest/jimm_mock.go index be96d6359..e97bb096e 100644 --- a/internal/jimmtest/jimm_mock.go +++ b/internal/jimmtest/jimm_mock.go @@ -130,7 +130,7 @@ func (j *JIMM) AddController(ctx context.Context, u *openfga.User, ctl *dbmodel. return j.AddController_(ctx, u, ctl) } func (j *JIMM) AddGroup(ctx context.Context, u *openfga.User, name string) error { - if j.AddGroup == nil { + if j.AddGroup_ == nil { return errors.E(errors.CodeNotImplemented) } return j.AddGroup_(ctx, u, name) From 75c2a2a2c26ede4e12635858b1d551a943a31656 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:51:45 +0000 Subject: [PATCH 063/126] Css 6715/implement get device session token (#1157) --- api/params/params.go | 31 +++-- cmd/jimmsrv/main.go | 33 +++--- docker-compose.yaml | 5 +- internal/auth/oauth2.go | 68 +++++++---- internal/auth/oauth2_test.go | 75 +++++------- internal/cmdtest/jimmsuite.go | 4 +- internal/dbmodel/identity.go | 5 + internal/dbmodel/sql/postgres/1_6.sql | 4 +- internal/jimm/jimm.go | 12 ++ internal/jimm/user.go | 48 ++++++++ internal/jimm/user_test.go | 68 +++++++++++ internal/jimmjwx/utils_test.go | 4 +- internal/jimmtest/jimm_mock.go | 7 ++ internal/jimmtest/keycloak.go | 2 +- internal/jimmtest/suite.go | 8 +- internal/jujuapi/admin.go | 162 ++++++++++++++++++++++++++ internal/jujuapi/admin_test.go | 65 +++++++++-- internal/jujuapi/controllerroot.go | 7 ++ local/keycloak/jimm-realm.json | 16 +-- service.go | 20 ++-- service_test.go | 32 ++--- 21 files changed, 535 insertions(+), 141 deletions(-) diff --git a/api/params/params.go b/api/params/params.go index 1b94b47c2..8c8d8bf62 100644 --- a/api/params/params.go +++ b/api/params/params.go @@ -402,16 +402,31 @@ type LoginDeviceResponse struct { VerificationURI string `json:"verification-uri"` // UserCode holds the one-time use user consent code. UserCode string `json:"user-code"` - // DeviceLoginID contains the login id to be sent to GetDeviceAccessToken in - // order to begin a CLI based short-lived session. - DeviceLoginID string `json:"device-login-id"` } -// LoginDeviceAccessTokenRequest holds no parameters to initiate a device login. -type LoginDeviceAccessTokenRequest struct{} - -// LoginDeviceAccessTokenResponse TODO -type LoginDeviceAccessTokenResponse struct{} +// GetDeviceSessionTokenResponse returns a session token to be used against +// LoginWithSessionToken for authentication. The session token will be base64 +// encoded. +type GetDeviceSessionTokenResponse struct { + // SessionToken is a base64 encoded JWT capable of authenticating + // a user. The JWT contains the users email address in the subject, + // and this is used to identify this user. + SessionToken string `json:"session-token"` +} + +// LoginWithSessionTokenRequest accepts a session token minted by JIMM and logs +// the user in. +// +// The login response for this login request type is that of jujuparams.LoginResult, +// such that the behaviour of previous macroon based authentication is unchanged. +// However, on unauthenticated requests, the error is different and is not a macaroon +// discharge request. +type LoginWithSessionTokenRequest struct { + // SessionToken is a base64 encoded JWT capable of authenticating + // a user. The JWT contains the users email address in the subject, + // and this is used to identify this user. + SessionToken string `json:"session-token"` +} // Service Account related request parameters diff --git a/cmd/jimmsrv/main.go b/cmd/jimmsrv/main.go index 60701d2ea..36eb1de48 100644 --- a/cmd/jimmsrv/main.go +++ b/cmd/jimmsrv/main.go @@ -92,20 +92,26 @@ func start(ctx context.Context, s *service.Service) error { return errors.E("oauth issuer url has no scheme") } - deviceClientID := os.Getenv("JIMM_OAUTH_DEVICE_CLIENT_ID") - if deviceClientID == "" { - zapctx.Error(ctx, "no oauth device client id") - return errors.E("no oauth device client id") + clientID := os.Getenv("JIMM_OAUTH_CLIENT_ID") + if clientID == "" { + zapctx.Error(ctx, "no oauth client id") + return errors.E("no oauth client id") } - deviceScopes := os.Getenv("JIMM_OAUTH_DEVICE_SCOPES") - deviceScopesParsed := strings.Split(deviceScopes, ",") - for i, scope := range deviceScopesParsed { - deviceScopesParsed[i] = strings.TrimSpace(scope) + clientSecret := os.Getenv("JIMM_OAUTH_CLIENT_SECRET") + if clientSecret == "" { + zapctx.Error(ctx, "no oauth client secret") + return errors.E("no oauth client secret") } - if len(deviceScopesParsed) == 0 { - zapctx.Error(ctx, "no oauth device client scopes present") - return errors.E("no oauth device client scopes present") + + scopes := os.Getenv("JIMM_OAUTH_SCOPES") + scopesParsed := strings.Split(scopes, ",") + for i, scope := range scopesParsed { + scopesParsed[i] = strings.TrimSpace(scope) + } + if len(scopesParsed) == 0 { + zapctx.Error(ctx, "no oauth client scopes present") + return errors.E("no oauth client scopes present") } insecureSecretStorage := false @@ -141,8 +147,9 @@ func start(ctx context.Context, s *service.Service) error { InsecureSecretStorage: insecureSecretStorage, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ IssuerURL: issuerURL, - DeviceClientID: deviceClientID, - DeviceScopes: deviceScopesParsed, + ClientID: clientID, + ClientSecret: clientSecret, + Scopes: scopesParsed, SessionTokenExpiry: sessionTokenExpiryDuration, }, }) diff --git a/docker-compose.yaml b/docker-compose.yaml index 5978d8c1a..2e663a1c0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -75,8 +75,9 @@ services: OPENFGA_AUTH_MODEL: "01GP1EC038KHGB6JJ2XXXXCXKB" OPENFGA_TOKEN: "jimm" JIMM_OAUTH_ISSUER_URL: "http://keycloak:8082/realms/jimm" # Scheme required - JIMM_OAUTH_DEVICE_CLIENT_ID: "jimm-device" # Must be a public client, no client secret - JIMM_OAUTH_DEVICE_SCOPES: "openid, profile, email" # Comma separated list of scopes + JIMM_OAUTH_CLIENT_ID: "jimm-device" + JIMM_OAUTH_CLIENT_SECRET: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4" + JIMM_OAUTH_SCOPES: "openid, profile, email" # Comma separated list of scopes volumes: - ./:/jimm/ - ./local/vault/approle.json:/vault/approle.json:rw diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index c63b1da8f..81cfbb216 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -4,6 +4,8 @@ package auth import ( "context" + "encoding/base64" + stderrors "errors" "net/mail" "time" @@ -19,7 +21,7 @@ import ( // AuthenticationService handles authentication within JIMM. type AuthenticationService struct { - deviceConfig oauth2.Config + oauthConfig oauth2.Config // provider holds a OIDC provider wrapper for the OAuth2.0 /x/oauth package, // enabling UserInfo calls, wellknown retrieval and jwks verification. provider *oidc.Provider @@ -33,12 +35,13 @@ type AuthenticationServiceParams struct { // IssuerURL is the URL of the OAuth2.0 server. // I.e., http://localhost:8082/realms/jimm in the case of keycloak. IssuerURL string - // DeviceClientID holds the OAuth2.0 client id registered and configured - // to handle device OAuth2.0 flows. The client is NOT expected to be confidential - // and as such does not need a client secret (given it is configured correctly). - DeviceClientID string - // DeviceScopes holds the scopes that you wish to retrieve. - DeviceScopes []string + // ClientID holds the OAuth2.0 client id. The client IS expected to be confidential. + ClientID string + // ClientSecret holds the OAuth2.0 "client-secret" to authenticate when performing + // /auth and /token requests. + ClientSecret string + // Scopes holds the scopes that you wish to retrieve. + Scopes []string // SessionTokenExpiry holds the expiry time of minted JIMM session tokens (JWTs). SessionTokenExpiry time.Duration } @@ -56,10 +59,11 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP return &AuthenticationService{ provider: provider, - deviceConfig: oauth2.Config{ - ClientID: params.DeviceClientID, - Endpoint: provider.Endpoint(), - Scopes: params.DeviceScopes, + oauthConfig: oauth2.Config{ + ClientID: params.ClientID, + ClientSecret: params.ClientSecret, + Endpoint: provider.Endpoint(), + Scopes: params.Scopes, }, sessionTokenExpiry: params.SessionTokenExpiry, }, nil @@ -82,7 +86,10 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP func (as *AuthenticationService) Device(ctx context.Context) (*oauth2.DeviceAuthResponse, error) { const op = errors.Op("auth.AuthenticationService.Device") - resp, err := as.deviceConfig.DeviceAuth(ctx) + resp, err := as.oauthConfig.DeviceAuth( + ctx, + oauth2.SetAuthURLParam("client_secret", as.oauthConfig.ClientSecret), + ) if err != nil { zapctx.Error(ctx, "device auth call failed", zap.Error(err)) return nil, errors.E(op, err, "device auth call failed") @@ -98,7 +105,11 @@ func (as *AuthenticationService) Device(ctx context.Context) (*oauth2.DeviceAuth func (as *AuthenticationService) DeviceAccessToken(ctx context.Context, res *oauth2.DeviceAuthResponse) (*oauth2.Token, error) { const op = errors.Op("auth.AuthenticationService.DeviceAccessToken") - t, err := as.deviceConfig.DeviceAccessToken(ctx, res) + t, err := as.oauthConfig.DeviceAccessToken( + ctx, + res, + oauth2.SetAuthURLParam("client_secret", as.oauthConfig.ClientSecret), + ) if err != nil { return nil, errors.E(op, err, "device access token call failed") } @@ -118,7 +129,7 @@ func (as *AuthenticationService) ExtractAndVerifyIDToken(ctx context.Context, oa } verifier := as.provider.Verifier(&oidc.Config{ - ClientID: as.deviceConfig.ClientID, + ClientID: as.oauthConfig.ClientID, }) token, err := verifier.Verify(ctx, rawIDToken) @@ -150,7 +161,7 @@ func (as *AuthenticationService) Email(idToken *oidc.IDToken) (string, error) { // MintSessionToken mints a session token to be used when logging into JIMM // via an access token. The token only contains the user's email for authentication. -func (as *AuthenticationService) MintSessionToken(email string, secretKey string) ([]byte, error) { +func (as *AuthenticationService) MintSessionToken(email string, secretKey string) (string, error) { const op = errors.Op("auth.AuthenticationService.MintAccessToken") token, err := jwt.NewBuilder(). @@ -158,26 +169,39 @@ func (as *AuthenticationService) MintSessionToken(email string, secretKey string Expiration(time.Now().Add(as.sessionTokenExpiry)). Build() if err != nil { - return nil, errors.E(op, err, "failed to build access token") + return "", errors.E(op, err, "failed to build access token") } freshToken, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte(secretKey))) if err != nil { - return nil, errors.E(op, err, "failed to sign access token") + return "", errors.E(op, err, "failed to sign access token") } - return freshToken, nil + + return base64.StdEncoding.EncodeToString(freshToken), nil } -// VerifyAccessToken symmetrically verifies the validty of the signature on the +// VerifySessionToken symmetrically verifies the validty of the signature on the // access token JWT, returning the parsed token. // // The subject of the token contains the user's email and can be used // for user object creation. -func (as *AuthenticationService) VerifyAccessToken(token []byte, secretKey string) (jwt.Token, error) { - const op = errors.Op("auth.AuthenticationService.VerifyAccessToken") +func (as *AuthenticationService) VerifySessionToken(token string, secretKey string) (jwt.Token, error) { + const op = errors.Op("auth.AuthenticationService.VerifySessionToken") + + if len(token) == 0 { + return nil, errors.E(op, "authentication failed, no token presented") + } + + decodedToken, err := base64.StdEncoding.DecodeString(token) + if err != nil { + return nil, errors.E(op, "authentication failed, failed to decode token") + } - parsedToken, err := jwt.Parse(token, jwt.WithKey(jwa.HS256, []byte(secretKey))) + parsedToken, err := jwt.Parse(decodedToken, jwt.WithKey(jwa.HS256, []byte(secretKey))) if err != nil { + if stderrors.Is(err, jwt.ErrTokenExpired()) { + return nil, errors.E(op, "JIMM session token expired") + } return nil, errors.E(op, err) } diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index d99d5f7e4..3a9b50c47 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -19,6 +19,19 @@ import ( qt "github.com/frankban/quicktest" ) +func setupTestAuthSvc(ctx context.Context, c *qt.C, expiry time.Duration) *auth.AuthenticationService { + authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: expiry, + }) + c.Assert(err, qt.IsNil) + + return authSvc +} + // TestDevice is a unique test in that it runs through the entire device oauth2.0 // flow and additionally ensures the id token is verified and correct. // @@ -35,12 +48,7 @@ func TestDevice(t *testing.T) { ctx := context.Background() - authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - }) - c.Assert(err, qt.IsNil) + authSvc := setupTestAuthSvc(ctx, c, time.Hour) res, err := authSvc.Device(ctx) c.Assert(err, qt.IsNil) @@ -113,95 +121,70 @@ func TestDevice(t *testing.T) { c.Assert(email, qt.Equals, u.Email) } -// TestAccessTokens tests both the minting and validation of JIMM -// access tokens. -func TestAccessTokens(t *testing.T) { +// TestSessionTokens tests both the minting and validation of JIMM +// session tokens. +func TestSessionTokens(t *testing.T) { c := qt.New(t) ctx := context.Background() - authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Hour, - }) - c.Assert(err, qt.IsNil) + authSvc := setupTestAuthSvc(ctx, c, time.Hour) secretKey := "secret-key" token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) c.Assert(err, qt.IsNil) c.Assert(len(token) > 0, qt.IsTrue) - jwtToken, err := authSvc.VerifyAccessToken(token, secretKey) + jwtToken, err := authSvc.VerifySessionToken(token, secretKey) c.Assert(err, qt.IsNil) c.Assert(jwtToken.Subject(), qt.Equals, "jimm-test@canonical.com") } -func TestAccessTokenRejectsWrongSecretKey(t *testing.T) { +func TestSessionTokenRejectsWrongSecretKey(t *testing.T) { c := qt.New(t) ctx := context.Background() - authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Hour, - }) - c.Assert(err, qt.IsNil) + authSvc := setupTestAuthSvc(ctx, c, time.Hour) secretKey := "secret-key" token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) c.Assert(err, qt.IsNil) c.Assert(len(token) > 0, qt.IsTrue) - _, err = authSvc.VerifyAccessToken(token, "wrong key") + _, err = authSvc.VerifySessionToken(token, "wrong key") c.Assert(err, qt.ErrorMatches, "could not verify message using any of the signatures or keys") } -func TestAccessTokenRejectsExpiredToken(t *testing.T) { +func TestSessionTokenRejectsExpiredToken(t *testing.T) { c := qt.New(t) ctx := context.Background() noDuration := time.Duration(0) - - authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: noDuration, - }) - c.Assert(err, qt.IsNil) + authSvc := setupTestAuthSvc(ctx, c, noDuration) secretKey := "secret-key" token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) c.Assert(err, qt.IsNil) c.Assert(len(token) > 0, qt.IsTrue) - _, err = authSvc.VerifyAccessToken(token, secretKey) - c.Assert(err, qt.ErrorMatches, `"exp" not satisfied`) + _, err = authSvc.VerifySessionToken(token, secretKey) + c.Assert(err, qt.ErrorMatches, `JIMM session token expired`) } -func TestAccessTokenValidatesEmail(t *testing.T) { +func TestSessionTokenValidatesEmail(t *testing.T) { c := qt.New(t) ctx := context.Background() - authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Hour, - }) - c.Assert(err, qt.IsNil) + authSvc := setupTestAuthSvc(ctx, c, time.Hour) secretKey := "secret-key" token, err := authSvc.MintSessionToken("", secretKey) c.Assert(err, qt.IsNil) c.Assert(len(token) > 0, qt.IsTrue) - _, err = authSvc.VerifyAccessToken(token, secretKey) + _, err = authSvc.VerifySessionToken(token, secretKey) c.Assert(err, qt.ErrorMatches, "failed to parse email") } diff --git a/internal/cmdtest/jimmsuite.go b/internal/cmdtest/jimmsuite.go index da6b36f89..fdf041d1f 100644 --- a/internal/cmdtest/jimmsuite.go +++ b/internal/cmdtest/jimmsuite.go @@ -87,8 +87,8 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { InsecureSecretStorage: true, OAuthAuthenticatorParams: service.OAuthAuthenticatorParams{ IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, } diff --git a/internal/dbmodel/identity.go b/internal/dbmodel/identity.go index 56ebe0bf6..8f6e30e38 100644 --- a/internal/dbmodel/identity.go +++ b/internal/dbmodel/identity.go @@ -34,6 +34,11 @@ type Identity struct { // CloudCredentials are the cloud credentials owned by this identity. CloudCredentials []CloudCredential `gorm:"foreignKey:OwnerIdentityName;references:Name"` + + // AccessToken is an OAuth2.0 access token for this identity, it may have come + // from the browser or device flow, and as such is updated on every successful + // login. + AccessToken string } // Tag returns a names.Tag for the identity. diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql index b93ff7530..62f17acea 100644 --- a/internal/dbmodel/sql/postgres/1_6.sql +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -1,4 +1,6 @@ --- 1_6.sql is a migration that renames `user` to `identity`. +-- 1_6.sql is a migration that adds access tokens to the user table +-- and is a migration that renames `user` to `identity`. +ALTER TABLE users ADD COLUMN access_token TEXT; -- Note that we don't need to rename underlying indexes/constraints. As Postgres -- docs states: diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index fdb17e99b..b43182256 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -17,6 +17,7 @@ import ( jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v4" "github.com/juju/zaputil/zapctx" + "github.com/lestrrat-go/jwx/v2/jwt" "go.uber.org/zap" "golang.org/x/oauth2" "golang.org/x/sync/errgroup" @@ -153,6 +154,17 @@ type OAuthAuthenticator interface { // Email retrieves the users email from an id token via the email claim Email(idToken *oidc.IDToken) (string, error) + + // MintSessionToken mints a session token to be used when logging into JIMM + // via an access token. The token only contains the user's email for authentication. + MintSessionToken(email string, secretKey string) (string, error) + + // VerifySessionToken symmetrically verifies the validty of the signature on the + // access token JWT, returning the parsed token. + // + // The subject of the token contains the user's email and can be used + // for user object creation. + VerifySessionToken(token string, secretKey string) (jwt.Token, error) } type permission struct { diff --git a/internal/jimm/user.go b/internal/jimm/user.go index f373e893d..a3afcaedc 100644 --- a/internal/jimm/user.go +++ b/internal/jimm/user.go @@ -60,6 +60,54 @@ func (j *JIMM) Authenticate(ctx context.Context, req *jujuparams.LoginRequest) ( return u, nil } +// GetOpenFGAUserAndAuthorise returns a valid OpenFGA user, authorising +// them as an admin of JIMM if a tuple exists for this user. +func (j *JIMM) GetOpenFGAUserAndAuthorise(ctx context.Context, email string) (*openfga.User, error) { + const op = errors.Op("jimm.GetOpenFGAUserAndAuthorise") + + // Setup identity model using the tag to populate query fields + user := &dbmodel.Identity{ + // TODO(ale8k): Name is email for NOW until we add email field + // and map emails/usernames to a uuid for the user. Then, queries should be + // queried upon by uuid, not username. + Name: email, + } + + // Load the users details + if err := j.Database.Transaction(func(tx *db.Database) error { + if err := tx.GetIdentity(ctx, user); err != nil { + return err + } + + // TODO(ale8k): + // This logic of updating the users last login should be done else where + // and not in the retrieval of the user, ideally a new db method + // to update login times and tokens. For now, it's ok, but it should be + // moved. + user.LastLogin.Time = j.Database.DB.Config.NowFunc() + user.LastLogin.Valid = true + + return tx.UpdateIdentity(ctx, user) + }); err != nil { + return nil, errors.E(op, err) + } + + // Wrap the user in OpenFGA user for administrator check & ready to place + // on controllerRoot.user + ofgaUser := openfga.NewUser(user, j.AuthorizationClient()) + + // Check if user is admin + isJimmAdmin, err := openfga.IsAdministrator(ctx, ofgaUser, j.ResourceTag()) + if err != nil { + return nil, errors.E(op, err) + } + + // Set the users admin status for the lifecycle of this WS + ofgaUser.JimmAdmin = isJimmAdmin + + return ofgaUser, nil +} + // GetUser fetches the user specified by the username and returns // an openfga User that can be used to verify user's permissions. func (j *JIMM) GetUser(ctx context.Context, username string) (*openfga.User, error) { diff --git a/internal/jimm/user_test.go b/internal/jimm/user_test.go index f6ef6c934..8ea5467d4 100644 --- a/internal/jimm/user_test.go +++ b/internal/jimm/user_test.go @@ -12,6 +12,7 @@ import ( jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v4" + "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" @@ -97,3 +98,70 @@ func TestAuthenticate(t *testing.T) { c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeUnauthorized) c.Check(u, qt.IsNil) } + +func TestGetOpenFGAUser(t *testing.T) { + c := qt.New(t) + + ctx := context.Background() + + // Test setup + client, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) + c.Assert(err, qt.IsNil) + + // TODO(ale8k): Mock this + authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{"openid", "profile", "email"}, + SessionTokenExpiry: time.Hour, + }) + c.Assert(err, qt.IsNil) + + now := time.Now().UTC().Round(time.Millisecond) + j := &jimm.JIMM{ + UUID: "test", + Database: db.Database{ + DB: jimmtest.PostgresDB(c, func() time.Time { return now }), + }, + OAuthAuthenticator: authSvc, + OpenFGAClient: client, + } + + err = j.Database.Migrate(ctx, false) + c.Assert(err, qt.IsNil) + + // Get the OpenFGA variant of the user + ofgaUser, err := j.GetOpenFGAUserAndAuthorise(ctx, "bob@external.com") + c.Assert(err, qt.IsNil) + // Username -> email + c.Assert(ofgaUser.Name, qt.Equals, "bob@external.com") + // As no display name was set for this user as they're being created this time over + c.Assert(ofgaUser.DisplayName, qt.Equals, "") + // The last login should be updated, so we check if it's been updated + // in the last second (for general accuracy when testing) + c.Assert((time.Since(ofgaUser.LastLogin.Time) > time.Second), qt.IsFalse) + // Ensure last login was valid + c.Assert(ofgaUser.LastLogin.Valid, qt.IsTrue) + // This user SHOULD NOT be an admin, so ensure admin check is OK + c.Assert(ofgaUser.JimmAdmin, qt.IsFalse) + + // Next we'll update this user to an admin of JIMM and run the same tests. + c.Assert( + ofgaUser.SetControllerAccess( + context.Background(), + names.NewControllerTag(j.UUID), + ofganames.AdministratorRelation, + ), + qt.IsNil, + ) + + ofgaUser, err = j.GetOpenFGAUserAndAuthorise(ctx, "bob@external.com") + c.Assert(err, qt.IsNil) + + c.Assert(ofgaUser.Name, qt.Equals, "bob@external.com") + c.Assert(ofgaUser.DisplayName, qt.Equals, "") + c.Assert((time.Since(ofgaUser.LastLogin.Time) > time.Second), qt.IsFalse) + c.Assert(ofgaUser.LastLogin.Valid, qt.IsTrue) + // This user SHOULD be an admin, so ensure admin check is OK + c.Assert(ofgaUser.JimmAdmin, qt.IsTrue) +} diff --git a/internal/jimmjwx/utils_test.go b/internal/jimmjwx/utils_test.go index a80a3fbad..e0a6d47f1 100644 --- a/internal/jimmjwx/utils_test.go +++ b/internal/jimmjwx/utils_test.go @@ -110,8 +110,8 @@ func setupService(ctx context.Context, c *qt.C) (*jimm.Service, *httptest.Server }, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, }) diff --git a/internal/jimmtest/jimm_mock.go b/internal/jimmtest/jimm_mock.go index e97bb096e..4b448a023 100644 --- a/internal/jimmtest/jimm_mock.go +++ b/internal/jimmtest/jimm_mock.go @@ -61,6 +61,7 @@ type JIMM struct { GetControllerConfig_ func(ctx context.Context, u *dbmodel.Identity) (*dbmodel.ControllerConfig, error) GetJimmControllerAccess_ func(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error) GetUser_ func(ctx context.Context, username string) (*openfga.User, error) + GetOpenFGAUserAndAuthorise_ func(ctx context.Context, email string) (*openfga.User, error) GetUserCloudAccess_ func(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error) GetUserControllerAccess_ func(ctx context.Context, user *openfga.User, controller names.ControllerTag) (string, error) GetUserModelAccess_ func(ctx context.Context, user *openfga.User, model names.ModelTag) (string, error) @@ -311,6 +312,12 @@ func (j *JIMM) GetUser(ctx context.Context, username string) (*openfga.User, err } return j.GetUser(ctx, username) } +func (j *JIMM) GetOpenFGAUserAndAuthorise(ctx context.Context, email string) (*openfga.User, error) { + if j.GetOpenFGAUserAndAuthorise_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.GetUser(ctx, email) +} func (j *JIMM) GetUserCloudAccess(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error) { if j.GetUserCloudAccess_ == nil { return "", errors.E(errors.CodeNotImplemented) diff --git a/internal/jimmtest/keycloak.go b/internal/jimmtest/keycloak.go index dcb912c30..c52092318 100644 --- a/internal/jimmtest/keycloak.go +++ b/internal/jimmtest/keycloak.go @@ -39,7 +39,7 @@ type KeycloakUser struct { // CreateRandomKeycloakUser creates a Keycloak user with random username and // returns the created user details. func CreateRandomKeycloakUser() (*KeycloakUser, error) { - username := "random_user_" + uuid.New().String()[0:8] + username := "random-user-" + uuid.New().String()[0:8] email := username + "@canonical.com" password := "jimm" diff --git a/internal/jimmtest/suite.go b/internal/jimmtest/suite.go index 18fe1b875..2ed4b81af 100644 --- a/internal/jimmtest/suite.go +++ b/internal/jimmtest/suite.go @@ -89,9 +89,11 @@ func (s *JIMMSuite) SetUpTest(c *gc.C) { // Connects to a pre-configured keycloak realm authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Hour, }) c.Assert(err, gc.Equals, nil) s.JIMM.OAuthAuthenticator = authSvc diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index ff1b1f14c..50aad4bc1 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -6,6 +6,7 @@ import ( "context" stderrors "errors" "sort" + "strings" "github.com/juju/juju/rpc" jujuparams "github.com/juju/juju/rpc/params" @@ -13,7 +14,9 @@ import ( "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/auth" + "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" + "github.com/canonical/jimm/internal/openfga" "github.com/canonical/jimm/internal/servermon" ) @@ -94,6 +97,9 @@ func (r *controllerRoot) LoginDevice(ctx context.Context) (params.LoginDeviceRes return response, errors.E(op, err) } + // NOTE: As this is on the controller root struct, and a new controller root + // is created per WS, it is EXPECTED that the subsequent call to GetDeviceSessionToken + // happens on the SAME websocket. r.deviceOAuthResponse = deviceResponse response.VerificationURI = deviceResponse.VerificationURI @@ -101,3 +107,159 @@ func (r *controllerRoot) LoginDevice(ctx context.Context) (params.LoginDeviceRes return response, nil } + +// GetDeviceSessionToken retrieves an access token from the OIDC provider +// and wraps it into a JWT, using the id token's email claim for the subject +// of the JWT. This in turn will be used for authentication against LoginWithSessionToken, +// where the subject of the JWT contains the user's email - enabling identification +// of the said user's session. +func (r *controllerRoot) GetDeviceSessionToken(ctx context.Context) (params.GetDeviceSessionTokenResponse, error) { + const op = errors.Op("jujuapi.GetDeviceSessionToken") + response := params.GetDeviceSessionTokenResponse{} + authSvc := r.jimm.OAuthAuthenticationService() + + token, err := authSvc.DeviceAccessToken(ctx, r.deviceOAuthResponse) + if err != nil { + return response, errors.E(op, err) + } + + idToken, err := authSvc.ExtractAndVerifyIDToken(ctx, token) + if err != nil { + return response, errors.E(op, err) + } + + email, err := authSvc.Email(idToken) + if err != nil { + return response, errors.E(op, err) + } + + // TODO(ale8k): Move this into a service, don't do db logic + // at the handler level + // Now we know who the user is, i.e., their email + // we'll update their access token. + // + // Build username + display name + db := r.jimm.DB() + u := &dbmodel.Identity{ + Name: email, + } + // TODO(babakks): If user does not exist, we will create one with an empty + // display name (which we shouldn't). So it would be better to fetch + // and then create. At the moment, GetUser is used for both create and fetch, + // this should be changed and split apart so it is intentional what entities + // we are creating or fetching. + if err := db.GetIdentity(ctx, u); err != nil { + return response, errors.E(op, err) + } + // Check if user has a display name, if not, set one + if u.DisplayName == "" { + u.DisplayName = strings.Split(email, "@")[0] + } + u.AccessToken = token.AccessToken + if err := r.jimm.DB().UpdateIdentity(ctx, u); err != nil { + return response, errors.E(op, err) + } + // + + // TODO(ale8k): Add vault logic to get secret key and generate one + // on start up. + encToken, err := authSvc.MintSessionToken(email, "secret-key") + if err != nil { + return response, errors.E(op, err) + } + + response.SessionToken = string(encToken) + + return response, nil +} + +// LoginWithSessionToken handles logging into the JIMM via a session token that JIMM has +// minted itself, this session token is simply a JWT containing the users email +// at which point the email is used to perform a lookup for the user, authorise +// whether or not they're an admin and place the user on the controller root +// such that subsequent facade method calls can access the authenticated user. +func (r *controllerRoot) LoginWithSessionToken(ctx context.Context, req params.LoginWithSessionTokenRequest) (jujuparams.LoginResult, error) { + const op = errors.Op("jujuapi.LoginWithSessionToken") + authenticationSvc := r.jimm.OAuthAuthenticationService() + + // Verify the session token + jwtToken, err := authenticationSvc.VerifySessionToken(req.SessionToken, "secret-key") + if err != nil { + var aerr *auth.AuthenticationError + if stderrors.As(err, &aerr) { + return aerr.LoginResult, nil + } + return jujuparams.LoginResult{}, errors.E(op, err) + } + + // Get an OpenFGA user to place on the controllerRoot for this WS + // such that: + // + // - Subsequent calls are aware of the user + // - Authorisation checks are done against the openfga.User + email := jwtToken.Subject() + + // At this point, we know the user exists, so simply just get + // the user to create the session token. + user, err := r.jimm.GetOpenFGAUserAndAuthorise(ctx, email) + if err != nil { + return jujuparams.LoginResult{}, errors.E(op, err) + } + + // TODO(ale8k): This isn't needed I don't think as controller roots are unique + // per WS, but if anyone knows different please let me know. + r.mu.Lock() + r.user = user + r.mu.Unlock() + + // Get server version for LoginResult + srvVersion, err := r.jimm.EarliestControllerVersion(ctx) + if err != nil { + return jujuparams.LoginResult{}, errors.E(op, err) + } + + return jujuparams.LoginResult{ + PublicDNSName: r.params.PublicDNSName, + UserInfo: setupAuthUserInfo(ctx, r, user), + ControllerTag: setupControllerTag(r), + Facades: setupFacades(r), + ServerVersion: srvVersion.String(), + }, nil +} + +// setupControllerTag returns the String() of a controller tag based on the +// JIMM controller UUID. +func setupControllerTag(root *controllerRoot) string { + return names.NewControllerTag(root.params.ControllerUUID).String() +} + +// setupAuthUserInfo creates a user info object to embed into the LoginResult. +func setupAuthUserInfo(ctx context.Context, root *controllerRoot, user *openfga.User) *jujuparams.AuthUserInfo { + aui := jujuparams.AuthUserInfo{ + DisplayName: user.DisplayName, + Identity: user.Tag().String(), + // TODO(Kian) CSS-6040 improve combining Postgres and OpenFGA info + ControllerAccess: user.GetControllerAccess(ctx, root.jimm.ResourceTag()).String(), + } + if user.LastLogin.Valid { + aui.LastConnection = &user.LastLogin.Time + } + return &aui +} + +// setupFacades ranges over all facades JIMM is aware of and sorts them into +// a versioned slice to give back to the LoginResult. +func setupFacades(root *controllerRoot) []jujuparams.FacadeVersions { + var facades []jujuparams.FacadeVersions + for name, f := range facadeInit { + facades = append(facades, jujuparams.FacadeVersions{ + Name: name, + Versions: f(root), + }) + } + sort.Slice(facades, func(i, j int) bool { + return facades[i].Name < facades[j].Name + }) + return facades + +} diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index 0f3ac621f..59a3d5ed6 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -3,14 +3,19 @@ package jujuapi_test import ( + "context" + "encoding/base64" "fmt" "io" "net/http" "net/http/cookiejar" "net/url" "regexp" + "strings" "github.com/canonical/jimm/api/params" + "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/jimmtest" "github.com/juju/juju/api" jujuparams "github.com/juju/juju/rpc/params" gc "gopkg.in/check.v1" @@ -55,6 +60,10 @@ func (s *adminSuite) TestDeviceLogin(c *gc.C) { }, "test") defer conn.Close() + // Create a user in keycloak + user, err := jimmtest.CreateRandomKeycloakUser() + c.Assert(err, gc.IsNil) + // We create a http client to keep the same cookies across all requests // using a simple jar. jar, err := cookiejar.New(nil) @@ -80,15 +89,15 @@ func (s *adminSuite) TestDeviceLogin(c *gc.C) { // // A complete URI looks like: http://localhost:8082/realms/jimm/device?user_code=HOKO-OTRV // where the user code is set as a part of the query string. - var resp params.LoginDeviceResponse - err = conn.APICall("Admin", 4, "", "LoginDevice", nil, &resp) + var loginDeviceResp params.LoginDeviceResponse + err = conn.APICall("Admin", 4, "", "LoginDevice", nil, &loginDeviceResp) c.Assert(err, gc.IsNil) - c.Assert(resp.UserCode, gc.Not(gc.IsNil)) - c.Assert(resp.VerificationURI, gc.Equals, "http://localhost:8082/realms/jimm/device") + c.Assert(loginDeviceResp.UserCode, gc.Not(gc.IsNil)) + c.Assert(loginDeviceResp.VerificationURI, gc.Equals, "http://localhost:8082/realms/jimm/device") // Step 2, complete the user side of the authentication by sending the // user code to the verification URI using the "complete" method. - userResp, err := client.Get(resp.VerificationURI + "?user_code=" + resp.UserCode) + userResp, err := client.Get(loginDeviceResp.VerificationURI + "?user_code=" + loginDeviceResp.UserCode) c.Assert(err, gc.IsNil) body := userResp.Body defer body.Close() @@ -97,13 +106,51 @@ func (s *adminSuite) TestDeviceLogin(c *gc.C) { loginForm := string(b) // Step 2.1, handle the login form (see this func for more details) - handleLoginForm(c, loginForm, client) + handleLoginForm(c, loginForm, client, user.Username, user.Password) + + // Step 3, after the user has entered the user code, the polling for an access + // token will complete. The polling can begin before OR after the user has entered the + // user code, for the simplicity of testing, we are retrieving it AFTER. + var sessionTokenResp params.GetDeviceSessionTokenResponse + err = conn.APICall("Admin", 4, "", "GetDeviceSessionToken", nil, &sessionTokenResp) + c.Assert(err, gc.IsNil) + // Ensure it is base64 and decodable + decodedToken, err := base64.StdEncoding.DecodeString(sessionTokenResp.SessionToken) + c.Assert(err, gc.IsNil) + + // Step 4, use this session token to "login". + + // Test no token present + var loginResult jujuparams.LoginResult + err = conn.APICall("Admin", 4, "", "LoginWithSessionToken", nil, &loginResult) + c.Assert(err, gc.ErrorMatches, "authentication failed, no token presented") + + // Test token not base64 encoded + err = conn.APICall("Admin", 4, "", "LoginWithSessionToken", params.LoginWithSessionTokenRequest{SessionToken: string(decodedToken)}, &loginResult) + c.Assert(err, gc.ErrorMatches, "authentication failed, failed to decode token") + + // Test token base64 encoded passes authentication + err = conn.APICall("Admin", 4, "", "LoginWithSessionToken", params.LoginWithSessionTokenRequest{SessionToken: sessionTokenResp.SessionToken}, &loginResult) + c.Assert(err, gc.IsNil) + c.Assert(loginResult.UserInfo.Identity, gc.Equals, "user-"+user.Email) + c.Assert(loginResult.UserInfo.DisplayName, gc.Equals, strings.Split(user.Email, "@")[0]) + + // Finally, ensure db did indeed update the access token for this user + updatedUser := &dbmodel.Identity{ + Name: user.Email, + } + c.Assert(s.JIMM.DB().GetIdentity(context.Background(), updatedUser), gc.IsNil) + // TODO(ale8k): Do we need to validate the token again for the test? + // It has just been through a verifier etc and was returned directly + // from the device grant? + c.Assert(updatedUser.AccessToken, gc.Not(gc.Equals), "") + } // handleLoginForm runs through the login process emulating the user typing in // their username and password and then clicking consent, to complete // the device login flow. -func handleLoginForm(c *gc.C, loginForm string, client *http.Client) { +func handleLoginForm(c *gc.C, loginForm string, client *http.Client, username, password string) { // Step 2.2, now we'll be redirected to a sign-in page and must sign in. re := regexp.MustCompile(`action="(.*?)" method=`) match := re.FindStringSubmatch(loginForm) @@ -112,8 +159,8 @@ func handleLoginForm(c *gc.C, loginForm string, client *http.Client) { // The username and password are hardcoded witih jimm-realm.json in our local // keycloak configuration for the jimm realm. v := url.Values{} - v.Add("username", "jimm-test") - v.Add("password", "password") + v.Add("username", username) + v.Add("password", password) loginResp, err := client.PostForm(loginFormUrl, v) c.Assert(err, gc.IsNil) diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index 8390d4875..ef7a01385 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -104,6 +104,7 @@ type JIMM interface { UpdateMigratedModel(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetControllerName string) error ValidateModelUpgrade(ctx context.Context, u *openfga.User, mt names.ModelTag, force bool) error WatchAllModelSummaries(ctx context.Context, controller *dbmodel.Controller) (_ func() error, err error) + GetOpenFGAUserAndAuthorise(ctx context.Context, email string) (*openfga.User, error) } // controllerRoot is the root for endpoints served on controller connections. @@ -124,6 +125,10 @@ type controllerRoot struct { // deviceOAuthResponse holds a device code flow response for this request, // such that JIMM can retrieve the access and ID tokens via polling the Authentication // Service's issuer via the /token endpoint. + // + // NOTE: As this is on the controller root struct, and a new controller root + // is created per WS, it is EXPECTED that the subsequent call to GetDeviceSessionToken + // happens on the SAME websocket. deviceOAuthResponse *oauth2.DeviceAuthResponse } @@ -144,6 +149,8 @@ func newControllerRoot(j JIMM, p Params) *controllerRoot { r.AddMethod("Admin", 3, "Login", rpc.Method(r.Login)) r.AddMethod("Admin", 4, "Login", rpc.Method(r.Login)) r.AddMethod("Admin", 4, "LoginDevice", rpc.Method(r.LoginDevice)) + r.AddMethod("Admin", 4, "GetDeviceSessionToken", rpc.Method(r.GetDeviceSessionToken)) + r.AddMethod("Admin", 4, "LoginWithSessionToken", rpc.Method(r.LoginWithSessionToken)) r.AddMethod("Pinger", 1, "Ping", rpc.Method(r.Ping)) return r } diff --git a/local/keycloak/jimm-realm.json b/local/keycloak/jimm-realm.json index f06f63022..b78f88f2e 100644 --- a/local/keycloak/jimm-realm.json +++ b/local/keycloak/jimm-realm.json @@ -688,7 +688,6 @@ ] }, { - "id": "9b8ea7fd-1ea8-43e6-9e2e-71b8f7d878e3", "clientId": "jimm-device", "name": "jimm-testing", "description": "A client to enable testing JIMM", @@ -699,6 +698,7 @@ "enabled": true, "alwaysDisplayInConsole": true, "clientAuthenticatorType": "client-secret", + "secret": "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", "redirectUris": [ "http://localhost/cb" ], @@ -712,12 +712,12 @@ "implicitFlowEnabled": false, "directAccessGrantsEnabled": true, "serviceAccountsEnabled": false, - "publicClient": true, + "publicClient": false, "frontchannelLogout": true, "protocol": "openid-connect", "attributes": { "oidc.ciba.grant.enabled": "false", - "client.secret.creation.time": "1705363608", + "client.secret.creation.time": "1707471625", "backchannel.logout.session.required": "true", "consent.screen.text": "JIMM consent screen", "login_theme": "base", @@ -731,7 +731,6 @@ "nodeReRegistrationTimeout": -1, "protocolMappers": [ { - "id": "28687e0a-ff13-4a16-b8a8-80bd6bbccea9", "name": "Client Host", "protocol": "openid-connect", "protocolMapper": "oidc-usersessionmodel-note-mapper", @@ -747,7 +746,6 @@ } }, { - "id": "1f6015f3-2812-41a9-896d-155d7d685ca6", "name": "Client IP Address", "protocol": "openid-connect", "protocolMapper": "oidc-usersessionmodel-note-mapper", @@ -763,7 +761,6 @@ } }, { - "id": "5f8f6d15-2f4d-4ce0-908b-9c5fd482e1b5", "name": "Client ID", "protocol": "openid-connect", "protocolMapper": "oidc-usersessionmodel-note-mapper", @@ -791,7 +788,12 @@ "phone", "offline_access", "microprofile-jwt" - ] + ], + "access": { + "view": true, + "configure": true, + "manage": true + } }, { "id": "7ec82fa5-d1f5-4e74-b438-e9be4fa32f17", diff --git a/service.go b/service.go index 16426ef4d..541de6992 100644 --- a/service.go +++ b/service.go @@ -68,12 +68,13 @@ type OAuthAuthenticatorParams struct { // IssuerURL is the URL of the OAuth2.0 server. // I.e., http://localhost:8082/realms/jimm in the case of keycloak. IssuerURL string - // DeviceClientID holds the OAuth2.0 client id registered and configured - // to handle device OAuth2.0 flows. The client is NOT expected to be confidential - // and as such does not need a client secret (given it is configured correctly). - DeviceClientID string - // DeviceScopes holds the scopes that you wish to retrieve. - DeviceScopes []string + // ClientID holds the OAuth2.0. The client IS expected to be confidential. + ClientID string + // ClientSecret holds the OAuth2.0 "client-secret" to authenticate when performing + // /auth and /token requests. + ClientSecret string + // Scopes holds the scopes that you wish to retrieve. + Scopes []string // SessionTokenExpiry holds the expiry duration for issued JWTs // for user (CLI) to JIMM authentication. SessionTokenExpiry time.Duration @@ -299,9 +300,10 @@ func NewService(ctx context.Context, p Params) (*Service, error) { s.jimm.OAuthAuthenticator, err = auth.NewAuthenticationService( ctx, auth.AuthenticationServiceParams{ - IssuerURL: p.OAuthAuthenticatorParams.IssuerURL, - DeviceClientID: p.OAuthAuthenticatorParams.DeviceClientID, - DeviceScopes: p.OAuthAuthenticatorParams.DeviceScopes, + IssuerURL: p.OAuthAuthenticatorParams.IssuerURL, + ClientID: p.OAuthAuthenticatorParams.ClientID, + ClientSecret: p.OAuthAuthenticatorParams.ClientSecret, + Scopes: p.OAuthAuthenticatorParams.Scopes, }, ) if err != nil { diff --git a/service_test.go b/service_test.go index af94bc2bf..d225acdbc 100644 --- a/service_test.go +++ b/service_test.go @@ -52,8 +52,8 @@ func TestDefaultService(t *testing.T) { InsecureSecretStorage: true, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, }) @@ -76,8 +76,8 @@ func TestServiceStartsWithoutSecretStore(t *testing.T) { OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, }) @@ -98,8 +98,8 @@ func TestAuthenticator(t *testing.T) { InsecureSecretStorage: true, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, } @@ -167,8 +167,8 @@ func TestVault(t *testing.T) { OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, } @@ -236,8 +236,8 @@ func TestPostgresSecretStore(t *testing.T) { InsecureSecretStorage: true, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, } @@ -258,8 +258,8 @@ func TestOpenFGA(t *testing.T) { ControllerAdmins: []string{"alice", "eve"}, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, } @@ -314,8 +314,8 @@ func TestPublicKey(t *testing.T) { PublicKey: "pC8MEk9MS9S8fhyRnOJ4qARTcTAwoM9L1nH/Yq0MwWU=", OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, } @@ -402,8 +402,8 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { PublicKey: "pC8MEk9MS9S8fhyRnOJ4qARTcTAwoM9L1nH/Yq0MwWU=", OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ IssuerURL: "http://localhost:8082/realms/jimm", - DeviceClientID: "jimm-device", - DeviceScopes: []string{oidc.ScopeOpenID, "profile", "email"}, + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, } From 9ebe220d8564023de09003affbb29430ce5403f0 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:07:41 +0200 Subject: [PATCH 064/126] CSS-7044 Added mock authenticator (#1160) * Added empty mock authenticator * Tweaks * Further tweaks for tests that need real authenticator * PR changes * Godoc fix --- internal/auth/oauth2.go | 11 +++++++++-- internal/jimmtest/auth.go | 18 ++++++++++++++++++ internal/jimmtest/keycloak.go | 3 +++ internal/jimmtest/suite.go | 13 ++----------- internal/jujuapi/admin_test.go | 19 +++++++++++++++++++ 5 files changed, 51 insertions(+), 13 deletions(-) diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 81cfbb216..54b040be9 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -180,12 +180,19 @@ func (as *AuthenticationService) MintSessionToken(email string, secretKey string return base64.StdEncoding.EncodeToString(freshToken), nil } +// VerifySessionToken calls the exported VerifySessionToken function. +func (as *AuthenticationService) VerifySessionToken(token string, secretKey string) (jwt.Token, error) { + return VerifySessionToken(token, secretKey) +} + // VerifySessionToken symmetrically verifies the validty of the signature on the // access token JWT, returning the parsed token. // // The subject of the token contains the user's email and can be used -// for user object creation. -func (as *AuthenticationService) VerifySessionToken(token string, secretKey string) (jwt.Token, error) { +// for user object creation +// +// This method is exported for use by the mock authenticator. +func VerifySessionToken(token string, secretKey string) (jwt.Token, error) { const op = errors.Op("auth.AuthenticationService.VerifySessionToken") if len(token) == 0 { diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index 0d9750ef5..3c99613f1 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -6,7 +6,10 @@ import ( "context" jujuparams "github.com/juju/juju/rpc/params" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/canonical/jimm/internal/auth" + "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/openfga" ) @@ -21,3 +24,18 @@ type Authenticator struct { func (a Authenticator) Authenticate(_ context.Context, _ *jujuparams.LoginRequest) (*openfga.User, error) { return a.User, a.Err } + +type MockOAuthAuthenticator struct { + jimm.OAuthAuthenticator + secretKey string +} + +func NewMockOAuthAuthenticator(secretKey string) MockOAuthAuthenticator { + return MockOAuthAuthenticator{secretKey: secretKey} +} + +// VerifySessionToken provides the mock implementation for verifying session tokens. +// Allowing JIMM tests to create their own session tokens that will always be accepted. +func (m MockOAuthAuthenticator) VerifySessionToken(token string, secretKey string) (jwt.Token, error) { + return auth.VerifySessionToken(token, m.secretKey) +} diff --git a/internal/jimmtest/keycloak.go b/internal/jimmtest/keycloak.go index c52092318..005ce950b 100644 --- a/internal/jimmtest/keycloak.go +++ b/internal/jimmtest/keycloak.go @@ -50,15 +50,18 @@ func CreateRandomKeycloakUser() (*KeycloakUser, error) { } if err := addKeycloakUser(adminCLIToken, email, username); err != nil { + zapctx.Error(context.Background(), "failed to add keycloak user", zap.Error(err)) return nil, errors.E(err, fmt.Sprintf("failed to add keycloak user (%q, %q)", email, username)) } id, err := getKeycloakUserId(adminCLIToken, username) if err != nil { + zapctx.Error(context.Background(), "failed to get keycloak user ID", zap.Error(err)) return nil, errors.E(err, fmt.Sprintf("failed to retrieve ID for newly added keycloak user (%q, %q)", email, username)) } if err := setKeycloakUserPassword(adminCLIToken, id, password); err != nil { + zapctx.Error(context.Background(), "failed to set keycloak user password", zap.Error(err)) return nil, errors.E(err, fmt.Sprintf("failed to set password for newly added keycloak user (%q, %q, %q)", email, username, password)) } return &KeycloakUser{ diff --git a/internal/jimmtest/suite.go b/internal/jimmtest/suite.go index 2ed4b81af..1003f3d4c 100644 --- a/internal/jimmtest/suite.go +++ b/internal/jimmtest/suite.go @@ -10,7 +10,6 @@ import ( "github.com/canonical/candid/candidtest" cofga "github.com/canonical/ofga" - "github.com/coreos/go-oidc/v3/oidc" "github.com/go-chi/chi/v5" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" @@ -87,16 +86,8 @@ func (s *JIMMSuite) SetUpTest(c *gc.C) { ctx, cancel := context.WithCancel(context.Background()) s.cancel = cancel - // Connects to a pre-configured keycloak realm - authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Hour, - }) - c.Assert(err, gc.Equals, nil) - s.JIMM.OAuthAuthenticator = authSvc + // Note that the secret key here must match what is used in tests. + s.JIMM.OAuthAuthenticator = NewMockOAuthAuthenticator("test-key") err = s.JIMM.Database.Migrate(ctx, false) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index 59a3d5ed6..c2e353a07 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -12,10 +12,13 @@ import ( "net/url" "regexp" "strings" + "time" "github.com/canonical/jimm/api/params" + "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/jimmtest" + "github.com/coreos/go-oidc/v3/oidc" "github.com/juju/juju/api" jujuparams "github.com/juju/juju/rpc/params" gc "gopkg.in/check.v1" @@ -26,6 +29,22 @@ type adminSuite struct { websocketSuite } +func (s *adminSuite) SetUpTest(c *gc.C) { + s.websocketSuite.SetUpTest(c) + ctx := context.Background() + // Replace JIMM's mock authenticator with a real one here + // for testing the login flows. + authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Hour, + }) + c.Assert(err, gc.Equals, nil) + s.JIMM.OAuthAuthenticator = authSvc +} + var _ = gc.Suite(&adminSuite{}) func (s *adminSuite) TestLoginToController(c *gc.C) { From b2bc53fd5917684654c35c8d9b0f7aaf1c582728 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:23:27 +0200 Subject: [PATCH 065/126] Adds extra tests for service account methods (#1161) --- internal/jujuapi/service_account_test.go | 96 ++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/internal/jujuapi/service_account_test.go b/internal/jujuapi/service_account_test.go index 9f1936930..34c05b004 100644 --- a/internal/jujuapi/service_account_test.go +++ b/internal/jujuapi/service_account_test.go @@ -214,6 +214,40 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), }}, + }, { + about: "Invalid Service account ID", + updateCloudCredential: func(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) { + return nil, nil + }, + args: params.UpdateServiceAccountCredentialsRequest{ + ClientID: "_123_", + UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ + Credentials: []jujuparams.TaggedCredential{ + { + Tag: "cloudcred-aws/1cbe5066-ea80-4979-8633-048d32f46cf8/cred-name", + Credential: jujuparams.CloudCredential{Attributes: map[string]string{"foo": "bar"}}, + }, + }}, + }, + username: "alice", + expectedError: "invalid client ID", + }, { + about: "Missing service account administrator permission", + updateCloudCredential: func(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) { + return nil, nil + }, + args: params.UpdateServiceAccountCredentialsRequest{ + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ + Credentials: []jujuparams.TaggedCredential{ + { + Tag: "cloudcred-aws/1cbe5066-ea80-4979-8633-048d32f46cf8/cred-name", + Credential: jujuparams.CloudCredential{Attributes: map[string]string{"foo": "bar"}}, + }, + }}, + }, + username: "alice", + expectedError: "unauthorized", }} for _, test := range tests { @@ -288,6 +322,40 @@ func TestListServiceAccountCredentials(t *testing.T) { Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), }}, + }, { + about: "Invalid Service account ID", + ForEachUserCloudCredential: func(ctx context.Context, u *dbmodel.Identity, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error { + return nil + }, + args: params.ListServiceAccountCredentialsRequest{ + ClientID: "_123_", + }, + getCloudCredential: func(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) { + cred := &dbmodel.CloudCredential{} + return cred, nil + }, + getCloudCredentialAttributes: func(ctx context.Context, u *openfga.User, cred *dbmodel.CloudCredential, hidden bool) (attrs map[string]string, redacted []string, err error) { + return nil, nil, nil + }, + username: "alice", + expectedError: "invalid client ID", + }, { + about: "Missing service account administrator permission", + ForEachUserCloudCredential: func(ctx context.Context, u *dbmodel.Identity, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error { + return nil + }, + args: params.ListServiceAccountCredentialsRequest{ + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + }, + getCloudCredential: func(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) { + cred := &dbmodel.CloudCredential{} + return cred, nil + }, + getCloudCredentialAttributes: func(ctx context.Context, u *openfga.User, cred *dbmodel.CloudCredential, hidden bool) (attrs map[string]string, redacted []string, err error) { + return nil, nil, nil + }, + username: "alice", + expectedError: "unauthorized", }} for _, test := range tests { @@ -357,6 +425,34 @@ func TestGrantServiceAccountAccess(t *testing.T) { Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), }}, + }, { + about: "Invalid Service account ID", + grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, svcAccTag jimmnames.ServiceAccountTag, entities []string) error { + return nil + }, + params: params.GrantServiceAccountAccess{ + Entities: []string{ + "user-alice", + "user-bob", + }, + ClientID: "_123_", + }, + username: "alice", + expectedError: "invalid client ID", + }, { + about: "Missing service account administrator permission", + grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, svcAccTag jimmnames.ServiceAccountTag, entities []string) error { + return nil + }, + params: params.GrantServiceAccountAccess{ + Entities: []string{ + "user-alice", + "user-bob", + }, + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + }, + username: "alice", + expectedError: "unauthorized", }} for _, test := range tests { From 9a059ea1efd80e4b5935bc1f51acbbf4899af510 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Tue, 27 Feb 2024 14:55:02 +0200 Subject: [PATCH 066/126] CSS-6718 add oauth relation to k8s charm (#1164) * Add OAuth relation to k8s charm * Fix tests * Ensure jimm DNS is an FQDN --- charms/jimm-k8s/config.yaml | 10 + charms/jimm-k8s/lib/charms/hydra/v0/oauth.py | 767 +++++++++++++++++++ charms/jimm-k8s/metadata.yaml | 3 + charms/jimm-k8s/requirements.txt | 1 + charms/jimm-k8s/src/charm.py | 46 +- charms/jimm-k8s/tests/unit/test_charm.py | 174 +++-- cmd/jimmsrv/main.go | 2 +- docker-compose.yaml | 2 +- 8 files changed, 930 insertions(+), 75 deletions(-) create mode 100644 charms/jimm-k8s/lib/charms/hydra/v0/oauth.py diff --git a/charms/jimm-k8s/config.yaml b/charms/jimm-k8s/config.yaml index 20d8241a5..bbf92dde2 100644 --- a/charms/jimm-k8s/config.yaml +++ b/charms/jimm-k8s/config.yaml @@ -85,7 +85,17 @@ options: type: string description: | Duration for the JWT expiry (defaults to 5 minutes). + This is the JWT JIMM sends to a Juju controller to authenticate + model related commands. Increase this if long running websocket + connections are failing due to authentication errors. default: 5m + session-expiry-duration: + type: string + default: 6h + description: | + Expiry duration for JIMM session tokens. These tokens are used + by clients and their expiry determines how frequently a user + must login. macaroon-expiry-duration: type: string default: 24h diff --git a/charms/jimm-k8s/lib/charms/hydra/v0/oauth.py b/charms/jimm-k8s/lib/charms/hydra/v0/oauth.py new file mode 100644 index 000000000..6d8ed1ef9 --- /dev/null +++ b/charms/jimm-k8s/lib/charms/hydra/v0/oauth.py @@ -0,0 +1,767 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""# Oauth Library. + +This library is designed to enable applications to register OAuth2/OIDC +clients with an OIDC Provider through the `oauth` interface. + +## Getting started + +To get started using this library you just need to fetch the library using `charmcraft`. **Note +that you also need to add `jsonschema` to your charm's `requirements.txt`.** + +```shell +cd some-charm +charmcraft fetch-lib charms.hydra.v0.oauth +EOF +``` + +Then, to initialize the library: +```python +# ... +from charms.hydra.v0.oauth import ClientConfig, OAuthRequirer + +OAUTH = "oauth" +OAUTH_SCOPES = "openid email" +OAUTH_GRANT_TYPES = ["authorization_code"] + +class SomeCharm(CharmBase): + def __init__(self, *args): + # ... + self.oauth = OAuthRequirer(self, client_config, relation_name=OAUTH) + + self.framework.observe(self.oauth.on.oauth_info_changed, self._configure_application) + # ... + + def _on_ingress_ready(self, event): + self.external_url = "https://example.com" + self._set_client_config() + + def _set_client_config(self): + client_config = ClientConfig( + urljoin(self.external_url, "/oauth/callback"), + OAUTH_SCOPES, + OAUTH_GRANT_TYPES, + ) + self.oauth.update_client_config(client_config) +``` +""" + +import inspect +import json +import logging +import re +from dataclasses import asdict, dataclass, field +from typing import Dict, List, Mapping, Optional + +import jsonschema +from ops.charm import ( + CharmBase, + RelationBrokenEvent, + RelationChangedEvent, + RelationCreatedEvent, + RelationDepartedEvent, +) +from ops.framework import EventBase, EventSource, Handle, Object, ObjectEvents +from ops.model import Relation, Secret, TooManyRelatedAppsError + +# The unique Charmhub library identifier, never change it +LIBID = "a3a301e325e34aac80a2d633ef61fe97" + +# Increment this major API version when introducing breaking changes +LIBAPI = 0 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 5 + +logger = logging.getLogger(__name__) + +DEFAULT_RELATION_NAME = "oauth" +ALLOWED_GRANT_TYPES = ["authorization_code", "refresh_token", "client_credentials"] +ALLOWED_CLIENT_AUTHN_METHODS = ["client_secret_basic", "client_secret_post"] +CLIENT_SECRET_FIELD = "secret" + +url_regex = re.compile( + r"(^http://)|(^https://)" # http:// or https:// + r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|" + r"[A-Z0-9-]{2,}\.?)|" # domain... + r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip + r"(?::\d+)?" # optional port + r"(?:/?|[/?]\S+)$", + re.IGNORECASE, +) + +OAUTH_PROVIDER_JSON_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://canonical.github.io/charm-relation-interfaces/interfaces/oauth/schemas/provider.json", + "type": "object", + "properties": { + "issuer_url": { + "type": "string", + }, + "authorization_endpoint": { + "type": "string", + }, + "token_endpoint": { + "type": "string", + }, + "introspection_endpoint": { + "type": "string", + }, + "userinfo_endpoint": { + "type": "string", + }, + "jwks_endpoint": { + "type": "string", + }, + "scope": { + "type": "string", + }, + "client_id": { + "type": "string", + }, + "client_secret_id": { + "type": "string", + }, + "groups": {"type": "string", "default": None}, + "ca_chain": {"type": "array", "items": {"type": "string"}, "default": []}, + }, + "required": [ + "issuer_url", + "authorization_endpoint", + "token_endpoint", + "introspection_endpoint", + "userinfo_endpoint", + "jwks_endpoint", + "scope", + ], +} +OAUTH_REQUIRER_JSON_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://canonical.github.io/charm-relation-interfaces/interfaces/oauth/schemas/requirer.json", + "type": "object", + "properties": { + "redirect_uri": { + "type": "string", + "default": None, + }, + "audience": {"type": "array", "default": [], "items": {"type": "string"}}, + "scope": {"type": "string", "default": None}, + "grant_types": { + "type": "array", + "default": None, + "items": { + "enum": ["authorization_code", "client_credentials", "refresh_token"], + "type": "string", + }, + }, + "token_endpoint_auth_method": { + "type": "string", + "enum": ["client_secret_basic", "client_secret_post"], + "default": "client_secret_basic", + }, + }, + "required": ["redirect_uri", "audience", "scope", "grant_types", "token_endpoint_auth_method"], +} + + +class ClientConfigError(Exception): + """Emitted when invalid client config is provided.""" + + +class DataValidationError(RuntimeError): + """Raised when data validation fails on relation data.""" + + +def _load_data(data: Mapping, schema: Optional[Dict] = None) -> Dict: + """Parses nested fields and checks whether `data` matches `schema`.""" + ret = {} + for k, v in data.items(): + try: + ret[k] = json.loads(v) + except json.JSONDecodeError: + ret[k] = v + + if schema: + _validate_data(ret, schema) + return ret + + +def _dump_data(data: Dict, schema: Optional[Dict] = None) -> Dict: + if schema: + _validate_data(data, schema) + + ret = {} + for k, v in data.items(): + if isinstance(v, (list, dict)): + try: + ret[k] = json.dumps(v) + except json.JSONDecodeError as e: + raise DataValidationError(f"Failed to encode relation json: {e}") + else: + ret[k] = v + return ret + + +class OAuthRelation(Object): + """A class containing helper methods for oauth relation.""" + + def _pop_relation_data(self, relation_id: Relation) -> None: + if not self.model.unit.is_leader(): + return + + if len(self.model.relations) == 0: + return + + relation = self.model.get_relation(self._relation_name, relation_id=relation_id) + if not relation or not relation.app: + return + + try: + for data in list(relation.data[self.model.app]): + relation.data[self.model.app].pop(data, "") + except Exception as e: + logger.info(f"Failed to pop the relation data: {e}") + + +def _validate_data(data: Dict, schema: Dict) -> None: + """Checks whether `data` matches `schema`. + + Will raise DataValidationError if the data is not valid, else return None. + """ + try: + jsonschema.validate(instance=data, schema=schema) + except jsonschema.ValidationError as e: + raise DataValidationError(data, schema) from e + + +@dataclass +class ClientConfig: + """Helper class containing a client's configuration.""" + + redirect_uri: str + scope: str + grant_types: List[str] + audience: List[str] = field(default_factory=lambda: []) + token_endpoint_auth_method: str = "client_secret_basic" + client_id: Optional[str] = None + + def validate(self) -> None: + """Validate the client configuration.""" + # Validate redirect_uri + if not re.match(url_regex, self.redirect_uri): + raise ClientConfigError(f"Invalid URL {self.redirect_uri}") + + if self.redirect_uri.startswith("http://"): + logger.warning("Provided Redirect URL uses http scheme. Don't do this in production") + + # Validate grant_types + for grant_type in self.grant_types: + if grant_type not in ALLOWED_GRANT_TYPES: + raise ClientConfigError( + f"Invalid grant_type {grant_type}, must be one " f"of {ALLOWED_GRANT_TYPES}" + ) + + # Validate client authentication methods + if self.token_endpoint_auth_method not in ALLOWED_CLIENT_AUTHN_METHODS: + raise ClientConfigError( + f"Invalid client auth method {self.token_endpoint_auth_method}, " + f"must be one of {ALLOWED_CLIENT_AUTHN_METHODS}" + ) + + def to_dict(self) -> Dict: + """Convert object to dict.""" + return {k: v for k, v in asdict(self).items() if v is not None} + + +@dataclass +class OauthProviderConfig: + """Helper class containing provider's configuration.""" + + issuer_url: str + authorization_endpoint: str + token_endpoint: str + introspection_endpoint: str + userinfo_endpoint: str + jwks_endpoint: str + scope: str + client_id: Optional[str] = None + client_secret: Optional[str] = None + groups: Optional[str] = None + ca_chain: Optional[str] = None + + @classmethod + def from_dict(cls, dic: Dict) -> "OauthProviderConfig": + """Generate OauthProviderConfig instance from dict.""" + return cls(**{k: v for k, v in dic.items() if k in inspect.signature(cls).parameters}) + + +class OAuthInfoChangedEvent(EventBase): + """Event to notify the charm that the information in the databag changed.""" + + def __init__(self, handle: Handle, client_id: str, client_secret_id: str): + super().__init__(handle) + self.client_id = client_id + self.client_secret_id = client_secret_id + + def snapshot(self) -> Dict: + """Save event.""" + return { + "client_id": self.client_id, + "client_secret_id": self.client_secret_id, + } + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + self.client_id = snapshot["client_id"] + self.client_secret_id = snapshot["client_secret_id"] + + +class InvalidClientConfigEvent(EventBase): + """Event to notify the charm that the client configuration is invalid.""" + + def __init__(self, handle: Handle, error: str): + super().__init__(handle) + self.error = error + + def snapshot(self) -> Dict: + """Save event.""" + return { + "error": self.error, + } + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + self.error = snapshot["error"] + + +class OAuthInfoRemovedEvent(EventBase): + """Event to notify the charm that the provider data was removed.""" + + def snapshot(self) -> Dict: + """Save event.""" + return {} + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + pass + + +class OAuthRequirerEvents(ObjectEvents): + """Event descriptor for events raised by `OAuthRequirerEvents`.""" + + oauth_info_changed = EventSource(OAuthInfoChangedEvent) + oauth_info_removed = EventSource(OAuthInfoRemovedEvent) + invalid_client_config = EventSource(InvalidClientConfigEvent) + + +class OAuthRequirer(OAuthRelation): + """Register an oauth client.""" + + on = OAuthRequirerEvents() + + def __init__( + self, + charm: CharmBase, + client_config: Optional[ClientConfig] = None, + relation_name: str = DEFAULT_RELATION_NAME, + ) -> None: + super().__init__(charm, relation_name) + self._charm = charm + self._relation_name = relation_name + self._client_config = client_config + events = self._charm.on[relation_name] + self.framework.observe(events.relation_created, self._on_relation_created_event) + self.framework.observe(events.relation_changed, self._on_relation_changed_event) + self.framework.observe(events.relation_broken, self._on_relation_broken_event) + + def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: + try: + self._update_relation_data(self._client_config, event.relation.id) + except ClientConfigError as e: + self.on.invalid_client_config.emit(e.args[0]) + + def _on_relation_broken_event(self, event: RelationBrokenEvent) -> None: + # Workaround for https://github.com/canonical/operator/issues/888 + self._pop_relation_data(event.relation.id) + if self.is_client_created(): + event.defer() + logger.info("Relation data still available. Deferring the event") + return + + # Notify the requirer that the relation data was removed + self.on.oauth_info_removed.emit() + + def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: + if not self.model.unit.is_leader(): + return + + data = event.relation.data[event.app] + if not data: + logger.info("No relation data available.") + return + + data = _load_data(data, OAUTH_PROVIDER_JSON_SCHEMA) + + client_id = data.get("client_id") + client_secret_id = data.get("client_secret_id") + if not client_id or not client_secret_id: + logger.info("OAuth Provider info is available, waiting for client to be registered.") + # The client credentials are not ready yet, so we do nothing + # This could mean that the client credentials were removed from the databag, + # but we don't allow that (for now), so we don't have to check for it. + return + + self.on.oauth_info_changed.emit(client_id, client_secret_id) + + def _update_relation_data( + self, client_config: Optional[ClientConfig], relation_id: Optional[int] = None + ) -> None: + if not self.model.unit.is_leader() or not client_config: + return + + if not isinstance(client_config, ClientConfig): + raise ValueError(f"Unexpected client_config type: {type(client_config)}") + + client_config.validate() + + try: + relation = self.model.get_relation( + relation_name=self._relation_name, relation_id=relation_id + ) + except TooManyRelatedAppsError: + raise RuntimeError("More than one relations are defined. Please provide a relation_id") + + if not relation or not relation.app: + return + + data = _dump_data(client_config.to_dict(), OAUTH_REQUIRER_JSON_SCHEMA) + relation.data[self.model.app].update(data) + + def is_client_created(self, relation_id: Optional[int] = None) -> bool: + """Check if the client has been created.""" + if len(self.model.relations) == 0: + return None + try: + relation = self.model.get_relation(self._relation_name, relation_id=relation_id) + except TooManyRelatedAppsError: + raise RuntimeError("More than one relations are defined. Please provide a relation_id") + + if not relation or not relation.app: + return None + + return ( + "client_id" in relation.data[relation.app] + and "client_secret_id" in relation.data[relation.app] + ) + + def get_provider_info(self, relation_id: Optional[int] = None) -> OauthProviderConfig: + """Get the provider information from the databag.""" + if len(self.model.relations) == 0: + return None + try: + relation = self.model.get_relation(self._relation_name, relation_id=relation_id) + except TooManyRelatedAppsError: + raise RuntimeError("More than one relations are defined. Please provide a relation_id") + if not relation or not relation.app: + return None + + data = relation.data[relation.app] + if not data: + logger.info("No relation data available.") + return + + data = _load_data(data, OAUTH_PROVIDER_JSON_SCHEMA) + + client_secret_id = data.get("client_secret_id") + if client_secret_id: + _client_secret = self.get_client_secret(client_secret_id) + client_secret = _client_secret.get_content()[CLIENT_SECRET_FIELD] + data["client_secret"] = client_secret + + oauth_provider = OauthProviderConfig.from_dict(data) + return oauth_provider + + def get_client_secret(self, client_secret_id: str) -> Secret: + """Get the client_secret.""" + client_secret = self.model.get_secret(id=client_secret_id) + return client_secret + + def update_client_config( + self, client_config: ClientConfig, relation_id: Optional[int] = None + ) -> None: + """Update the client config stored in the object.""" + self._client_config = client_config + self._update_relation_data(client_config, relation_id=relation_id) + + +class ClientCreatedEvent(EventBase): + """Event to notify the Provider charm to create a new client.""" + + def __init__( + self, + handle: Handle, + redirect_uri: str, + scope: str, + grant_types: List[str], + audience: List, + token_endpoint_auth_method: str, + relation_id: int, + ) -> None: + super().__init__(handle) + self.redirect_uri = redirect_uri + self.scope = scope + self.grant_types = grant_types + self.audience = audience + self.token_endpoint_auth_method = token_endpoint_auth_method + self.relation_id = relation_id + + def snapshot(self) -> Dict: + """Save event.""" + return { + "redirect_uri": self.redirect_uri, + "scope": self.scope, + "grant_types": self.grant_types, + "audience": self.audience, + "token_endpoint_auth_method": self.token_endpoint_auth_method, + "relation_id": self.relation_id, + } + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + self.redirect_uri = snapshot["redirect_uri"] + self.scope = snapshot["scope"] + self.grant_types = snapshot["grant_types"] + self.audience = snapshot["audience"] + self.token_endpoint_auth_method = snapshot["token_endpoint_auth_method"] + self.relation_id = snapshot["relation_id"] + + def to_client_config(self) -> ClientConfig: + """Convert the event information to a ClientConfig object.""" + return ClientConfig( + self.redirect_uri, + self.scope, + self.grant_types, + self.audience, + self.token_endpoint_auth_method, + ) + + +class ClientChangedEvent(EventBase): + """Event to notify the Provider charm that the client config changed.""" + + def __init__( + self, + handle: Handle, + redirect_uri: str, + scope: str, + grant_types: List, + audience: List, + token_endpoint_auth_method: str, + relation_id: int, + client_id: str, + ) -> None: + super().__init__(handle) + self.redirect_uri = redirect_uri + self.scope = scope + self.grant_types = grant_types + self.audience = audience + self.token_endpoint_auth_method = token_endpoint_auth_method + self.relation_id = relation_id + self.client_id = client_id + + def snapshot(self) -> Dict: + """Save event.""" + return { + "redirect_uri": self.redirect_uri, + "scope": self.scope, + "grant_types": self.grant_types, + "audience": self.audience, + "token_endpoint_auth_method": self.token_endpoint_auth_method, + "relation_id": self.relation_id, + "client_id": self.client_id, + } + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + self.redirect_uri = snapshot["redirect_uri"] + self.scope = snapshot["scope"] + self.grant_types = snapshot["grant_types"] + self.audience = snapshot["audience"] + self.token_endpoint_auth_method = snapshot["token_endpoint_auth_method"] + self.relation_id = snapshot["relation_id"] + self.client_id = snapshot["client_id"] + + def to_client_config(self) -> ClientConfig: + """Convert the event information to a ClientConfig object.""" + return ClientConfig( + self.redirect_uri, + self.scope, + self.grant_types, + self.audience, + self.token_endpoint_auth_method, + self.client_id, + ) + + +class ClientDeletedEvent(EventBase): + """Event to notify the Provider charm that the client was deleted.""" + + def __init__( + self, + handle: Handle, + relation_id: int, + ) -> None: + super().__init__(handle) + self.relation_id = relation_id + + def snapshot(self) -> Dict: + """Save event.""" + return {"relation_id": self.relation_id} + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + self.relation_id = snapshot["relation_id"] + + +class OAuthProviderEvents(ObjectEvents): + """Event descriptor for events raised by `OAuthProviderEvents`.""" + + client_created = EventSource(ClientCreatedEvent) + client_changed = EventSource(ClientChangedEvent) + client_deleted = EventSource(ClientDeletedEvent) + + +class OAuthProvider(OAuthRelation): + """A provider object for OIDC Providers.""" + + on = OAuthProviderEvents() + + def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME) -> None: + super().__init__(charm, relation_name) + self._charm = charm + self._relation_name = relation_name + + events = self._charm.on[relation_name] + self.framework.observe( + events.relation_changed, + self._get_client_config_from_relation_data, + ) + self.framework.observe( + events.relation_departed, + self._on_relation_departed, + ) + + def _get_client_config_from_relation_data(self, event: RelationChangedEvent) -> None: + if not self.model.unit.is_leader(): + return + + data = event.relation.data[event.app] + if not data: + logger.info("No requirer relation data available.") + return + + client_data = _load_data(data, OAUTH_REQUIRER_JSON_SCHEMA) + redirect_uri = client_data.get("redirect_uri") + scope = client_data.get("scope") + grant_types = client_data.get("grant_types") + audience = client_data.get("audience") + token_endpoint_auth_method = client_data.get("token_endpoint_auth_method") + + data = event.relation.data[self._charm.app] + if not data: + logger.info("No provider relation data available.") + return + provider_data = _load_data(data, OAUTH_PROVIDER_JSON_SCHEMA) + client_id = provider_data.get("client_id") + + relation_id = event.relation.id + + if client_id: + # Modify an existing client + self.on.client_changed.emit( + redirect_uri, + scope, + grant_types, + audience, + token_endpoint_auth_method, + relation_id, + client_id, + ) + else: + # Create a new client + self.on.client_created.emit( + redirect_uri, scope, grant_types, audience, token_endpoint_auth_method, relation_id + ) + + def _get_secret_label(self, relation: Relation) -> str: + return f"client_secret_{relation.id}" + + def _on_relation_departed(self, event: RelationDepartedEvent) -> None: + # Workaround for https://github.com/canonical/operator/issues/888 + self._pop_relation_data(event.relation.id) + + self._delete_juju_secret(event.relation) + self.on.client_deleted.emit(event.relation.id) + + def _create_juju_secret(self, client_secret: str, relation: Relation) -> Secret: + """Create a juju secret and grant it to a relation.""" + secret = {CLIENT_SECRET_FIELD: client_secret} + juju_secret = self.model.app.add_secret(secret, label=self._get_secret_label(relation)) + juju_secret.grant(relation) + return juju_secret + + def _delete_juju_secret(self, relation: Relation) -> None: + secret = self.model.get_secret(label=self._get_secret_label(relation)) + secret.remove_all_revisions() + + def set_provider_info_in_relation_data( + self, + issuer_url: str, + authorization_endpoint: str, + token_endpoint: str, + introspection_endpoint: str, + userinfo_endpoint: str, + jwks_endpoint: str, + scope: str, + groups: Optional[str] = None, + ca_chain: Optional[str] = None, + ) -> None: + """Put the provider information in the databag.""" + if not self.model.unit.is_leader(): + return + + data = { + "issuer_url": issuer_url, + "authorization_endpoint": authorization_endpoint, + "token_endpoint": token_endpoint, + "introspection_endpoint": introspection_endpoint, + "userinfo_endpoint": userinfo_endpoint, + "jwks_endpoint": jwks_endpoint, + "scope": scope, + } + if groups: + data["groups"] = groups + if ca_chain: + data["ca_chain"] = ca_chain + + for relation in self.model.relations[self._relation_name]: + relation.data[self.model.app].update(_dump_data(data)) + + def set_client_credentials_in_relation_data( + self, relation_id: int, client_id: str, client_secret: str + ) -> None: + """Put the client credentials in the databag.""" + if not self.model.unit.is_leader(): + return + + relation = self.model.get_relation(self._relation_name, relation_id) + if not relation or not relation.app: + return + # TODO: What if we are refreshing the client_secret? We need to add a + # new revision for that + secret = self._create_juju_secret(client_secret, relation) + data = dict(client_id=client_id, client_secret_id=secret.id) + relation.data[self.model.app].update(_dump_data(data)) diff --git a/charms/jimm-k8s/metadata.yaml b/charms/jimm-k8s/metadata.yaml index 703b28a22..5eef23279 100644 --- a/charms/jimm-k8s/metadata.yaml +++ b/charms/jimm-k8s/metadata.yaml @@ -58,6 +58,9 @@ requires: interface: loki_push_api optional: true limit: 1 + oauth: + interface: oauth + limit: 1 containers: jimm: diff --git a/charms/jimm-k8s/requirements.txt b/charms/jimm-k8s/requirements.txt index 7b6542db5..4d6a6257a 100644 --- a/charms/jimm-k8s/requirements.txt +++ b/charms/jimm-k8s/requirements.txt @@ -5,3 +5,4 @@ jsonschema >= 3.2.0 cryptography >= 3.4.8 hvac >= 0.11.0 requests >= 2.25.1 +jsonschema diff --git a/charms/jimm-k8s/src/charm.py b/charms/jimm-k8s/src/charm.py index 566db985d..1734dda29 100755 --- a/charms/jimm-k8s/src/charm.py +++ b/charms/jimm-k8s/src/charm.py @@ -19,6 +19,7 @@ import json import logging import socket +from urllib.parse import urljoin import hvac import requests @@ -27,6 +28,7 @@ DatabaseRequires, ) from charms.grafana_k8s.v0.grafana_dashboard import GrafanaDashboardProvider +from charms.hydra.v0.oauth import ClientConfig, OAuthInfoChangedEvent, OAuthRequirer from charms.loki_k8s.v0.loki_push_api import LogProxyConsumer from charms.nginx_ingress_integrator.v0.nginx_route import require_nginx_route from charms.openfga_k8s.v0.openfga import OpenFGARequires, OpenFGAStoreCreateEvent @@ -72,6 +74,10 @@ LOG_FILE = "/var/log/jimm" # This likely will just be JIMM's port. PROMETHEUS_PORT = 8080 +OAUTH = "oauth" +OAUTH_SCOPES = "openid email offline_access" +# TODO: Add "device_code" below once the charm interface supports it. +OAUTH_GRANT_TYPES = ["authorization_code", "refresh_token"] class DeferError(Exception): @@ -89,7 +95,10 @@ def __init__(self, *args): self._state = State(self.app, lambda: self.model.get_relation("peer")) self._unit_state = State(self.unit, lambda: self.model.get_relation("peer")) + self.oauth = OAuthRequirer(self, self._oauth_client_config, relation_name=OAUTH) + self.framework.observe(self.oauth.on.oauth_info_changed, self._on_oauth_info_changed) + self.framework.observe(self.oauth.on.oauth_info_removed, self._on_oauth_info_changed) self.framework.observe(self.on.peer_relation_changed, self._on_peer_relation_changed) self.framework.observe(self.on.jimm_pebble_ready, self._on_jimm_pebble_ready) self.framework.observe(self.on.config_changed, self._on_config_changed) @@ -199,6 +208,9 @@ def _on_jimm_pebble_ready(self, event): def _on_config_changed(self, event): self._update_workload(event) + def _on_oauth_info_changed(self, event: OAuthInfoChangedEvent): + self._update_workload(event) + @requires_state_setter def _on_leader_elected(self, event): if not self._state.private_key: @@ -235,6 +247,7 @@ def _update_workload(self, event): event.defer() return + self.oauth.update_client_config(client_config=self._oauth_client_config) self._ensure_bakery_agent_file(event) self._ensure_vault_file(event) if self.model.get_relation("vault") and not container.exists(self._vault_secret_filename): @@ -242,11 +255,18 @@ def _update_workload(self, event): self.unit.status = BlockedStatus("Vault relation present but vault setup is not ready yet") return + if not self.oauth.is_client_created(): + logger.warning("OAuth relation is not ready yet") + self.unit.status = BlockedStatus("Waiting for OAuth relation") + return + dns_name = self._get_dns_name(event) if not dns_name: logger.warning("dns name not set") return + oauth_provider_info = self.oauth.get_provider_info() + config_values = { "CANDID_PUBLIC_KEY": self.config.get("candid-public-key", ""), "CANDID_URL": self.config.get("candid-url", ""), @@ -265,8 +285,13 @@ def _update_workload(self, event): "OPENFGA_PORT": self._state.openfga_port, "PRIVATE_KEY": self.config.get("private-key", ""), "PUBLIC_KEY": self.config.get("public-key", ""), - "JIMM_JWT_EXPIRY": self.config.get("jwt-expiry", "5m"), + "JIMM_JWT_EXPIRY": self.config.get("jwt-expiry"), "JIMM_MACAROON_EXPIRY_DURATION": self.config.get("macaroon-expiry-duration", "24h"), + "JIMM_ACCESS_TOKEN_EXPIRY_DURATION": self.config.get("session-expiry-duration"), + "JIMM_OAUTH_ISSUER_URL": oauth_provider_info.issuer_url, + "JIMM_OAUTH_CLIENT_ID": oauth_provider_info.client_id, + "JIMM_OAUTH_CLIENT_SECRET": oauth_provider_info.client_secret, + "JIMM_OAUTH_SCOPES": oauth_provider_info.scope, } if self._state.dsn: config_values["JIMM_DSN"] = self._state.dsn @@ -735,6 +760,25 @@ def _on_create_authorization_model_action(self, event: ActionEvent): self._state.openfga_auth_model_id = authorization_model_id self._update_workload(event) + @property + def _oauth_client_config(self) -> ClientConfig: + dns = self.config.get("dns-name") + if dns is None or dns == "": + dns = "http://localhost" + dns = ensureFQDN(dns) + return ClientConfig( + urljoin(dns, "/oauth/callback"), + OAUTH_SCOPES, + OAUTH_GRANT_TYPES, + ) + + +def ensureFQDN(dns: str): # noqa: N802 + """Ensures a domain name has an https:// prefix.""" + if not dns.startswith("http"): + dns = "https://" + dns + return dns + def _json_data(event, key): logger.debug("getting relation data {}".format(key)) diff --git a/charms/jimm-k8s/tests/unit/test_charm.py b/charms/jimm-k8s/tests/unit/test_charm.py index 44306f157..3053aa224 100644 --- a/charms/jimm-k8s/tests/unit/test_charm.py +++ b/charms/jimm-k8s/tests/unit/test_charm.py @@ -15,6 +15,18 @@ from src.charm import JimmOperatorCharm +OAUTH_CLIENT_ID = "jimm_client_id" +OAUTH_CLIENT_SECRET = "test-secret" +OAUTH_PROVIDER_INFO = { + "authorization_endpoint": "https://example.oidc.com/oauth2/auth", + "introspection_endpoint": "https://example.oidc.com/admin/oauth2/introspect", + "issuer_url": "https://example.oidc.com", + "jwks_endpoint": "https://example.oidc.com/.well-known/jwks.json", + "scope": "openid profile email phone", + "token_endpoint": "https://example.oidc.com/oauth2/token", + "userinfo_endpoint": "https://example.oidc.com/userinfo", +} + MINIMAL_CONFIG = { "uuid": "1234567890", "candid-url": "test-candid-url", @@ -28,7 +40,6 @@ "JIMM_DASHBOARD_LOCATION": "https://jaas.ai/models", "JIMM_DNS_NAME": "juju-jimm-k8s-0.juju-jimm-k8s-endpoints.None.svc.cluster.local", "JIMM_ENABLE_JWKS_ROTATOR": "1", - "JIMM_JWT_EXPIRY": "5m", "JIMM_LISTEN_ADDR": ":8080", "JIMM_LOG_LEVEL": "info", "JIMM_UUID": "1234567890", @@ -37,9 +48,36 @@ "PUBLIC_KEY": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", "JIMM_AUDIT_LOG_RETENTION_PERIOD_IN_DAYS": "0", "JIMM_MACAROON_EXPIRY_DURATION": "24h", + "JIMM_JWT_EXPIRY": "5m", + "JIMM_ACCESS_TOKEN_EXPIRY_DURATION": "6h", + "JIMM_OAUTH_ISSUER_URL": OAUTH_PROVIDER_INFO["issuer_url"], + "JIMM_OAUTH_CLIENT_ID": OAUTH_CLIENT_ID, + "JIMM_OAUTH_CLIENT_SECRET": OAUTH_CLIENT_SECRET, + "JIMM_OAUTH_SCOPES": OAUTH_PROVIDER_INFO["scope"], } +def get_expected_plan(env): + return { + "services": { + "jimm": { + "summary": "JAAS Intelligent Model Manager", + "startup": "disabled", + "override": "replace", + "command": "/root/jimmsrv", + "environment": env, + } + }, + "checks": { + "jimm-check": { + "override": "replace", + "period": "1m", + "http": {"url": "http://localhost:8080/debug/status"}, + } + }, + } + + class MockExec: def wait_output(): return True @@ -64,8 +102,22 @@ def setUp(self): self.harness.add_relation_unit(jimm_id, "juju-jimm-k8s/1") self.harness.container_pebble_ready("jimm") - rel_id = self.harness.add_relation("ingress", "nginx-ingress") - self.harness.add_relation_unit(rel_id, "nginx-ingress/0") + self.ingress_rel_id = self.harness.add_relation("ingress", "nginx-ingress") + self.harness.add_relation_unit(self.ingress_rel_id, "nginx-ingress/0") + + self.oauth_rel_id = self.harness.add_relation("oauth", "hydra") + self.harness.add_relation_unit(self.oauth_rel_id, "hydra/0") + secret_id = self.harness.add_model_secret("hydra", {"secret": OAUTH_CLIENT_SECRET}) + self.harness.grant_secret(secret_id, "juju-jimm-k8s") + self.harness.update_relation_data( + self.oauth_rel_id, + "hydra", + { + "client_id": OAUTH_CLIENT_ID, + "client_secret_id": secret_id, + **OAUTH_PROVIDER_INFO, + }, + ) # import ipdb; ipdb.set_trace() def test_on_pebble_ready(self): @@ -77,20 +129,7 @@ def test_on_pebble_ready(self): # Check the that the plan was updated plan = self.harness.get_container_pebble_plan("jimm") - self.assertEqual( - plan.to_dict(), - { - "services": { - "jimm": { - "summary": "JAAS Intelligent Model Manager", - "startup": "disabled", - "override": "replace", - "command": "/root/jimmsrv", - "environment": EXPECTED_ENV, - } - } - }, - ) + self.assertEqual(plan.to_dict(), get_expected_plan(EXPECTED_ENV)) def test_on_config_changed(self): container = self.harness.model.unit.get_container("jimm") @@ -104,20 +143,7 @@ def test_on_config_changed(self): # Check the that the plan was updated plan = self.harness.get_container_pebble_plan("jimm") - self.assertEqual( - plan.to_dict(), - { - "services": { - "jimm": { - "summary": "JAAS Intelligent Model Manager", - "startup": "disabled", - "override": "replace", - "command": "/root/jimmsrv", - "environment": EXPECTED_ENV, - } - } - }, - ) + self.assertEqual(plan.to_dict(), get_expected_plan(EXPECTED_ENV)) def test_postgres_secret_storage_config(self): container = self.harness.model.unit.get_container("jimm") @@ -134,20 +160,50 @@ def test_postgres_secret_storage_config(self): plan = self.harness.get_container_pebble_plan("jimm") expected_env = EXPECTED_ENV.copy() expected_env.update({"INSECURE_SECRET_STORAGE": "enabled"}) - self.assertEqual( - plan.to_dict(), - { - "services": { - "jimm": { - "summary": "JAAS Intelligent Model Manager", - "startup": "disabled", - "override": "replace", - "command": "/root/jimmsrv", - "environment": expected_env, - } - } - }, + self.assertEqual(plan.to_dict(), get_expected_plan(expected_env)) + + def test_app_dns_address(self): + self.harness.update_config(MINIMAL_CONFIG) + self.harness.update_config({"dns-name": "jimm.com"}) + oauth_client = self.harness.charm._oauth_client_config + self.assertEqual(oauth_client.redirect_uri, "https://jimm.com/oauth/callback") + + def test_app_enters_block_states_if_oauth_relation_removed(self): + self.harness.update_config(MINIMAL_CONFIG) + self.harness.remove_relation(self.oauth_rel_id) + container = self.harness.model.unit.get_container("jimm") + # Emit the pebble-ready event for jimm + self.harness.charm.on.jimm_pebble_ready.emit(container) + + # Check the that the plan is empty + plan = self.harness.get_container_pebble_plan("jimm") + self.assertEqual(plan.to_dict(), {}) + self.assertEqual(self.harness.charm.unit.status.name, BlockedStatus.name) + self.assertEqual(self.harness.charm.unit.status.message, "Waiting for OAuth relation") + + def test_app_enters_block_state_if_oauth_relation_not_ready(self): + self.harness.update_config(MINIMAL_CONFIG) + self.harness.remove_relation(self.oauth_rel_id) + oauth_relation = self.harness.add_relation("oauth", "hydra") + self.harness.add_relation_unit(oauth_relation, "hydra/0") + secret_id = self.harness.add_model_secret("hydra", {"secret": OAUTH_CLIENT_SECRET}) + self.harness.grant_secret(secret_id, "juju-jimm-k8s") + # If the client-id is empty we should detect that the oauth relation is not ready. + # The readiness check is handled by the OAuth library. + self.harness.update_relation_data( + oauth_relation, + "hydra", + {"client_id": ""}, ) + container = self.harness.model.unit.get_container("jimm") + # Emit the pebble-ready event for jimm + self.harness.charm.on.jimm_pebble_ready.emit(container) + + # Check the that the plan is empty + plan = self.harness.get_container_pebble_plan("jimm") + self.assertEqual(plan.to_dict(), {}) + self.assertEqual(self.harness.charm.unit.status.name, BlockedStatus.name) + self.assertEqual(self.harness.charm.unit.status.message, "Waiting for OAuth relation") def test_bakery_configuration(self): container = self.harness.model.unit.get_container("jimm") @@ -171,20 +227,7 @@ def test_bakery_configuration(self): expected_env.update({"BAKERY_AGENT_FILE": "/root/config/agent.json"}) # Check the that the plan was updated plan = self.harness.get_container_pebble_plan("jimm") - self.assertEqual( - plan.to_dict(), - { - "services": { - "jimm": { - "summary": "JAAS Intelligent Model Manager", - "startup": "disabled", - "override": "replace", - "command": "/root/jimmsrv", - "environment": expected_env, - } - } - }, - ) + self.assertEqual(plan.to_dict(), get_expected_plan(expected_env)) agent_data = container.pull("/root/config/agent.json") agent_json = json.loads(agent_data.read()) self.assertEqual( @@ -211,20 +254,7 @@ def test_audit_log_retention_config(self): expected_env.update({"JIMM_AUDIT_LOG_RETENTION_PERIOD_IN_DAYS": "10"}) # Check the that the plan was updated plan = self.harness.get_container_pebble_plan("jimm") - self.assertEqual( - plan.to_dict(), - { - "services": { - "jimm": { - "summary": "JAAS Intelligent Model Manager", - "startup": "disabled", - "override": "replace", - "command": "/root/jimmsrv", - "environment": expected_env, - } - } - }, - ) + self.assertEqual(plan.to_dict(), get_expected_plan(expected_env)) def test_dashboard_relation_joined(self): harness = Harness(JimmOperatorCharm) diff --git a/cmd/jimmsrv/main.go b/cmd/jimmsrv/main.go index 36eb1de48..db7a665d2 100644 --- a/cmd/jimmsrv/main.go +++ b/cmd/jimmsrv/main.go @@ -105,7 +105,7 @@ func start(ctx context.Context, s *service.Service) error { } scopes := os.Getenv("JIMM_OAUTH_SCOPES") - scopesParsed := strings.Split(scopes, ",") + scopesParsed := strings.Split(scopes, " ") for i, scope := range scopesParsed { scopesParsed[i] = strings.TrimSpace(scope) } diff --git a/docker-compose.yaml b/docker-compose.yaml index 2e663a1c0..f094ebe80 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -77,7 +77,7 @@ services: JIMM_OAUTH_ISSUER_URL: "http://keycloak:8082/realms/jimm" # Scheme required JIMM_OAUTH_CLIENT_ID: "jimm-device" JIMM_OAUTH_CLIENT_SECRET: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4" - JIMM_OAUTH_SCOPES: "openid, profile, email" # Comma separated list of scopes + JIMM_OAUTH_SCOPES: "openid profile email" # Space separated list of scopes volumes: - ./:/jimm/ - ./local/vault/approle.json:/vault/approle.json:rw From 583d6eb8df8e97f646e557f22c50919e593354a7 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Wed, 28 Feb 2024 14:20:30 +0000 Subject: [PATCH 067/126] feat(oauth login browser): implements /auth/login (#1166) * feat(oauth login browser): implements /auth/login As discussed on call, we integration test only the handler and avoid mocks to see the behaviour is as expected. This is also true when we come to implements the callback logic and are required to start the flow from scratch. The current test in auth_handler_test will be updated to cover the entire flow when implementing /callback. As for the state todo, I need to see how to track the state between requests and the TODO will be completed in the /callback PR. 6646 * pr comments * pr comments --- internal/auth/oauth2.go | 17 +++++++ internal/auth/oauth2_test.go | 19 +++++++ internal/jimmhttp/auth_handler.go | 43 ++++++++++++++++ internal/jimmhttp/auth_handler_test.go | 68 ++++++++++++++++++++++++++ local/keycloak/jimm-realm.json | 8 +-- service.go | 7 ++- 6 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 internal/jimmhttp/auth_handler.go create mode 100644 internal/jimmhttp/auth_handler_test.go diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 54b040be9..4a9fc995f 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -44,6 +44,10 @@ type AuthenticationServiceParams struct { Scopes []string // SessionTokenExpiry holds the expiry time of minted JIMM session tokens (JWTs). SessionTokenExpiry time.Duration + // RedirectURL is the URL for handling the exchange of authorisation + // codes into access tokens (and id tokens), for JIMM, this is expected + // to be the servers own callback endpoint registered under /auth/callback. + RedirectURL string } // NewAuthenticationService returns a new authentication service for handling @@ -64,11 +68,24 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP ClientSecret: params.ClientSecret, Endpoint: provider.Endpoint(), Scopes: params.Scopes, + RedirectURL: params.RedirectURL, }, sessionTokenExpiry: params.SessionTokenExpiry, }, nil } +// AuthCodeURL returns a URL that will be used to redirect a browser to the identity provider. +func (as *AuthenticationService) AuthCodeURL() string { + // As we're not the browser creating the auth code url and then communicating back + // to the server, it is OK not to set a state as there's no communication + // between say many "tabs" and a JIMM deployment, but rather + // just JIMM creating the auth code URL itself, and then handling the exchanging + // itself. Of course, middleman attacks between the IdP and JIMM are possible, + // but we'd have much larger problems than an auth code interception at that + // point. As such, we're opting out of using auth code URL state. + return as.oauthConfig.AuthCodeURL("") +} + // Device initiates a device flow login and is step ONE of TWO. // // This is done via retrieving a: diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 3a9b50c47..37113ee5b 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -26,12 +26,31 @@ func setupTestAuthSvc(ctx context.Context, c *qt.C, expiry time.Duration) *auth. ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: expiry, + RedirectURL: "http://localhost:8080/auth/callback", }) c.Assert(err, qt.IsNil) return authSvc } +// This test requires the local docker compose to be running and keycloak +// to be available. +// +// TODO(ale8k): Use a mock for this and also device below, but future work??? +func TestAuthCodeURL(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + + authSvc := setupTestAuthSvc(ctx, c, time.Hour) + + url := authSvc.AuthCodeURL() + c.Assert( + url, + qt.Equals, + `http://localhost:8082/realms/jimm/protocol/openid-connect/auth?client_id=jimm-device&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth%2Fcallback&response_type=code&scope=openid+profile+email`, + ) +} + // TestDevice is a unique test in that it runs through the entire device oauth2.0 // flow and additionally ensures the id token is verified and correct. // diff --git a/internal/jimmhttp/auth_handler.go b/internal/jimmhttp/auth_handler.go new file mode 100644 index 000000000..4ec601e7f --- /dev/null +++ b/internal/jimmhttp/auth_handler.go @@ -0,0 +1,43 @@ +package jimmhttp + +import ( + "net/http" + + "github.com/go-chi/chi/v5" +) + +// OAuthHandler handles the oauth2.0 browser flow for JIMM. +// Implements jimmhttp.JIMMHttpHandler. +type OAuthHandler struct { + Router *chi.Mux + Authenticator BrowserOAuthAuthenticator +} + +// BrowserOAuthAuthenticator handles authorisation code authentication within JIMM +// via OIDC. +type BrowserOAuthAuthenticator interface { + // AuthCodeURL returns a URL that will be used to redirect a browser to the identity provider. + AuthCodeURL() string +} + +// NewOAuthHandler returns a new OAuth handler. +func NewOAuthHandler(authenticator BrowserOAuthAuthenticator) *OAuthHandler { + return &OAuthHandler{Router: chi.NewRouter(), Authenticator: authenticator} +} + +// Routes returns the grouped routers routes with group specific middlewares. +func (oah *OAuthHandler) Routes() chi.Router { + oah.SetupMiddleware() + oah.Router.Get("/login", oah.Login) + return oah.Router +} + +// SetupMiddleware applies middlewares. +func (oah *OAuthHandler) SetupMiddleware() { +} + +// Login handles /auth/login, +func (oah *OAuthHandler) Login(w http.ResponseWriter, r *http.Request) { + redirectURL := oah.Authenticator.AuthCodeURL() + http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect) +} diff --git a/internal/jimmhttp/auth_handler_test.go b/internal/jimmhttp/auth_handler_test.go new file mode 100644 index 000000000..186e705da --- /dev/null +++ b/internal/jimmhttp/auth_handler_test.go @@ -0,0 +1,68 @@ +package jimmhttp_test + +import ( + "context" + "math/rand" + "net" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/canonical/jimm/internal/auth" + "github.com/canonical/jimm/internal/jimmhttp" + + "github.com/coreos/go-oidc/v3/oidc" + qt "github.com/frankban/quicktest" +) + +func setupTestServer(c *qt.C) *httptest.Server { + // Create unstarted server to enable auth service + s := httptest.NewUnstartedServer(nil) + // Setup random port listener + minPort := 30000 + maxPort := 50000 + + port := strconv.Itoa(rand.Intn(maxPort-minPort+1) + minPort) + l, err := net.Listen("tcp", "localhost:"+port) + c.Assert(err, qt.IsNil) + // Set the listener with a random port + s.Listener = l + + // Remember redirect url to check it matches after test server starts + redirectURL := "http://127.0.0.1:" + port + "/auth/callback" + + authSvc, err := auth.NewAuthenticationService(context.Background(), auth.AuthenticationServiceParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Hour, + // Now we know the port the test server is running on + RedirectURL: redirectURL, + }) + c.Assert(err, qt.IsNil) + + r := jimmhttp.NewOAuthHandler(authSvc).Routes() + s.Config.Handler = r + + s.Start() + + // Ensure redirectURL is matching port on listener + c.Assert(s.URL+"/auth/callback", qt.Equals, redirectURL) + + return s +} + +// Login is simply expected to redirect the user +func TestAuthLogin(t *testing.T) { + c := qt.New(t) + + s := setupTestServer(c) + defer s.Close() + + res, err := http.Get(s.URL + "/login") + c.Assert(err, qt.IsNil) + c.Assert(res.StatusCode, qt.Equals, http.StatusOK) +} diff --git a/local/keycloak/jimm-realm.json b/local/keycloak/jimm-realm.json index b78f88f2e..f7ef299f4 100644 --- a/local/keycloak/jimm-realm.json +++ b/local/keycloak/jimm-realm.json @@ -691,16 +691,16 @@ "clientId": "jimm-device", "name": "jimm-testing", "description": "A client to enable testing JIMM", - "rootUrl": "http://localhost", - "adminUrl": "http://localhost", - "baseUrl": "http://localhost", + "rootUrl": "http://127.0.0.1", + "adminUrl": "http://127.0.0.1", + "baseUrl": "http://127.0.0.1", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": true, "clientAuthenticatorType": "client-secret", "secret": "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", "redirectUris": [ - "http://localhost/cb" + "http://127.0.0.1/*" ], "webOrigins": [ "*" diff --git a/service.go b/service.go index 541de6992..bcfcaee6c 100644 --- a/service.go +++ b/service.go @@ -297,7 +297,7 @@ func NewService(ctx context.Context, p Params) (*Service, error) { return nil, errors.E(op, err) } - s.jimm.OAuthAuthenticator, err = auth.NewAuthenticationService( + authSvc, err := auth.NewAuthenticationService( ctx, auth.AuthenticationServiceParams{ IssuerURL: p.OAuthAuthenticatorParams.IssuerURL, @@ -306,6 +306,7 @@ func NewService(ctx context.Context, p Params) (*Service, error) { Scopes: p.OAuthAuthenticatorParams.Scopes, }, ) + s.jimm.OAuthAuthenticator = authSvc if err != nil { zapctx.Error(ctx, "failed to setup authentication service", zap.Error(err)) return nil, errors.E(op, err, "failed to setup authentication service") @@ -349,6 +350,10 @@ func NewService(ctx context.Context, p Params) (*Service, error) { "/.well-known", wellknownapi.NewWellKnownHandler(s.jimm.CredentialStore), ) + mountHandler( + "/auth", + jimmhttp.NewOAuthHandler(authSvc), + ) params := jujuapi.Params{ ControllerUUID: p.ControllerUUID, From 30f9be23fa6dd010fe51be6dc0519d7ffdf04f54 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 1 Mar 2024 10:42:11 +0200 Subject: [PATCH 068/126] CSS-6717 add machine charm oauth (#1165) * WIP adding oauth relation * WIP fixing charm tests * Fix tests * Add missing session-expiry-duration config * Fix incorrect const * Remove env files when relations are broken --- charms/jimm/config.yaml | 7 + charms/jimm/lib/charms/hydra/v0/oauth.py | 767 +++++++++++++++++++++++ charms/jimm/metadata.yaml | 4 + charms/jimm/requirements.txt | 1 + charms/jimm/src/charm.py | 118 +++- charms/jimm/templates/jimm-oauth.env | 4 + charms/jimm/templates/jimm.env | 1 + charms/jimm/tests/test_charm.py | 99 ++- 8 files changed, 960 insertions(+), 41 deletions(-) create mode 100644 charms/jimm/lib/charms/hydra/v0/oauth.py create mode 100644 charms/jimm/templates/jimm-oauth.env diff --git a/charms/jimm/config.yaml b/charms/jimm/config.yaml index 0f7c3e489..0b87014de 100644 --- a/charms/jimm/config.yaml +++ b/charms/jimm/config.yaml @@ -81,3 +81,10 @@ options: type: string default: 24h description: Expiry duration for authentication macaroons. + session-expiry-duration: + type: string + default: 6h + description: | + Expiry duration for JIMM session tokens. These tokens are used + by clients and their expiry determines how frequently a user + must login. diff --git a/charms/jimm/lib/charms/hydra/v0/oauth.py b/charms/jimm/lib/charms/hydra/v0/oauth.py new file mode 100644 index 000000000..6d8ed1ef9 --- /dev/null +++ b/charms/jimm/lib/charms/hydra/v0/oauth.py @@ -0,0 +1,767 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""# Oauth Library. + +This library is designed to enable applications to register OAuth2/OIDC +clients with an OIDC Provider through the `oauth` interface. + +## Getting started + +To get started using this library you just need to fetch the library using `charmcraft`. **Note +that you also need to add `jsonschema` to your charm's `requirements.txt`.** + +```shell +cd some-charm +charmcraft fetch-lib charms.hydra.v0.oauth +EOF +``` + +Then, to initialize the library: +```python +# ... +from charms.hydra.v0.oauth import ClientConfig, OAuthRequirer + +OAUTH = "oauth" +OAUTH_SCOPES = "openid email" +OAUTH_GRANT_TYPES = ["authorization_code"] + +class SomeCharm(CharmBase): + def __init__(self, *args): + # ... + self.oauth = OAuthRequirer(self, client_config, relation_name=OAUTH) + + self.framework.observe(self.oauth.on.oauth_info_changed, self._configure_application) + # ... + + def _on_ingress_ready(self, event): + self.external_url = "https://example.com" + self._set_client_config() + + def _set_client_config(self): + client_config = ClientConfig( + urljoin(self.external_url, "/oauth/callback"), + OAUTH_SCOPES, + OAUTH_GRANT_TYPES, + ) + self.oauth.update_client_config(client_config) +``` +""" + +import inspect +import json +import logging +import re +from dataclasses import asdict, dataclass, field +from typing import Dict, List, Mapping, Optional + +import jsonschema +from ops.charm import ( + CharmBase, + RelationBrokenEvent, + RelationChangedEvent, + RelationCreatedEvent, + RelationDepartedEvent, +) +from ops.framework import EventBase, EventSource, Handle, Object, ObjectEvents +from ops.model import Relation, Secret, TooManyRelatedAppsError + +# The unique Charmhub library identifier, never change it +LIBID = "a3a301e325e34aac80a2d633ef61fe97" + +# Increment this major API version when introducing breaking changes +LIBAPI = 0 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 5 + +logger = logging.getLogger(__name__) + +DEFAULT_RELATION_NAME = "oauth" +ALLOWED_GRANT_TYPES = ["authorization_code", "refresh_token", "client_credentials"] +ALLOWED_CLIENT_AUTHN_METHODS = ["client_secret_basic", "client_secret_post"] +CLIENT_SECRET_FIELD = "secret" + +url_regex = re.compile( + r"(^http://)|(^https://)" # http:// or https:// + r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|" + r"[A-Z0-9-]{2,}\.?)|" # domain... + r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip + r"(?::\d+)?" # optional port + r"(?:/?|[/?]\S+)$", + re.IGNORECASE, +) + +OAUTH_PROVIDER_JSON_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://canonical.github.io/charm-relation-interfaces/interfaces/oauth/schemas/provider.json", + "type": "object", + "properties": { + "issuer_url": { + "type": "string", + }, + "authorization_endpoint": { + "type": "string", + }, + "token_endpoint": { + "type": "string", + }, + "introspection_endpoint": { + "type": "string", + }, + "userinfo_endpoint": { + "type": "string", + }, + "jwks_endpoint": { + "type": "string", + }, + "scope": { + "type": "string", + }, + "client_id": { + "type": "string", + }, + "client_secret_id": { + "type": "string", + }, + "groups": {"type": "string", "default": None}, + "ca_chain": {"type": "array", "items": {"type": "string"}, "default": []}, + }, + "required": [ + "issuer_url", + "authorization_endpoint", + "token_endpoint", + "introspection_endpoint", + "userinfo_endpoint", + "jwks_endpoint", + "scope", + ], +} +OAUTH_REQUIRER_JSON_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://canonical.github.io/charm-relation-interfaces/interfaces/oauth/schemas/requirer.json", + "type": "object", + "properties": { + "redirect_uri": { + "type": "string", + "default": None, + }, + "audience": {"type": "array", "default": [], "items": {"type": "string"}}, + "scope": {"type": "string", "default": None}, + "grant_types": { + "type": "array", + "default": None, + "items": { + "enum": ["authorization_code", "client_credentials", "refresh_token"], + "type": "string", + }, + }, + "token_endpoint_auth_method": { + "type": "string", + "enum": ["client_secret_basic", "client_secret_post"], + "default": "client_secret_basic", + }, + }, + "required": ["redirect_uri", "audience", "scope", "grant_types", "token_endpoint_auth_method"], +} + + +class ClientConfigError(Exception): + """Emitted when invalid client config is provided.""" + + +class DataValidationError(RuntimeError): + """Raised when data validation fails on relation data.""" + + +def _load_data(data: Mapping, schema: Optional[Dict] = None) -> Dict: + """Parses nested fields and checks whether `data` matches `schema`.""" + ret = {} + for k, v in data.items(): + try: + ret[k] = json.loads(v) + except json.JSONDecodeError: + ret[k] = v + + if schema: + _validate_data(ret, schema) + return ret + + +def _dump_data(data: Dict, schema: Optional[Dict] = None) -> Dict: + if schema: + _validate_data(data, schema) + + ret = {} + for k, v in data.items(): + if isinstance(v, (list, dict)): + try: + ret[k] = json.dumps(v) + except json.JSONDecodeError as e: + raise DataValidationError(f"Failed to encode relation json: {e}") + else: + ret[k] = v + return ret + + +class OAuthRelation(Object): + """A class containing helper methods for oauth relation.""" + + def _pop_relation_data(self, relation_id: Relation) -> None: + if not self.model.unit.is_leader(): + return + + if len(self.model.relations) == 0: + return + + relation = self.model.get_relation(self._relation_name, relation_id=relation_id) + if not relation or not relation.app: + return + + try: + for data in list(relation.data[self.model.app]): + relation.data[self.model.app].pop(data, "") + except Exception as e: + logger.info(f"Failed to pop the relation data: {e}") + + +def _validate_data(data: Dict, schema: Dict) -> None: + """Checks whether `data` matches `schema`. + + Will raise DataValidationError if the data is not valid, else return None. + """ + try: + jsonschema.validate(instance=data, schema=schema) + except jsonschema.ValidationError as e: + raise DataValidationError(data, schema) from e + + +@dataclass +class ClientConfig: + """Helper class containing a client's configuration.""" + + redirect_uri: str + scope: str + grant_types: List[str] + audience: List[str] = field(default_factory=lambda: []) + token_endpoint_auth_method: str = "client_secret_basic" + client_id: Optional[str] = None + + def validate(self) -> None: + """Validate the client configuration.""" + # Validate redirect_uri + if not re.match(url_regex, self.redirect_uri): + raise ClientConfigError(f"Invalid URL {self.redirect_uri}") + + if self.redirect_uri.startswith("http://"): + logger.warning("Provided Redirect URL uses http scheme. Don't do this in production") + + # Validate grant_types + for grant_type in self.grant_types: + if grant_type not in ALLOWED_GRANT_TYPES: + raise ClientConfigError( + f"Invalid grant_type {grant_type}, must be one " f"of {ALLOWED_GRANT_TYPES}" + ) + + # Validate client authentication methods + if self.token_endpoint_auth_method not in ALLOWED_CLIENT_AUTHN_METHODS: + raise ClientConfigError( + f"Invalid client auth method {self.token_endpoint_auth_method}, " + f"must be one of {ALLOWED_CLIENT_AUTHN_METHODS}" + ) + + def to_dict(self) -> Dict: + """Convert object to dict.""" + return {k: v for k, v in asdict(self).items() if v is not None} + + +@dataclass +class OauthProviderConfig: + """Helper class containing provider's configuration.""" + + issuer_url: str + authorization_endpoint: str + token_endpoint: str + introspection_endpoint: str + userinfo_endpoint: str + jwks_endpoint: str + scope: str + client_id: Optional[str] = None + client_secret: Optional[str] = None + groups: Optional[str] = None + ca_chain: Optional[str] = None + + @classmethod + def from_dict(cls, dic: Dict) -> "OauthProviderConfig": + """Generate OauthProviderConfig instance from dict.""" + return cls(**{k: v for k, v in dic.items() if k in inspect.signature(cls).parameters}) + + +class OAuthInfoChangedEvent(EventBase): + """Event to notify the charm that the information in the databag changed.""" + + def __init__(self, handle: Handle, client_id: str, client_secret_id: str): + super().__init__(handle) + self.client_id = client_id + self.client_secret_id = client_secret_id + + def snapshot(self) -> Dict: + """Save event.""" + return { + "client_id": self.client_id, + "client_secret_id": self.client_secret_id, + } + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + self.client_id = snapshot["client_id"] + self.client_secret_id = snapshot["client_secret_id"] + + +class InvalidClientConfigEvent(EventBase): + """Event to notify the charm that the client configuration is invalid.""" + + def __init__(self, handle: Handle, error: str): + super().__init__(handle) + self.error = error + + def snapshot(self) -> Dict: + """Save event.""" + return { + "error": self.error, + } + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + self.error = snapshot["error"] + + +class OAuthInfoRemovedEvent(EventBase): + """Event to notify the charm that the provider data was removed.""" + + def snapshot(self) -> Dict: + """Save event.""" + return {} + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + pass + + +class OAuthRequirerEvents(ObjectEvents): + """Event descriptor for events raised by `OAuthRequirerEvents`.""" + + oauth_info_changed = EventSource(OAuthInfoChangedEvent) + oauth_info_removed = EventSource(OAuthInfoRemovedEvent) + invalid_client_config = EventSource(InvalidClientConfigEvent) + + +class OAuthRequirer(OAuthRelation): + """Register an oauth client.""" + + on = OAuthRequirerEvents() + + def __init__( + self, + charm: CharmBase, + client_config: Optional[ClientConfig] = None, + relation_name: str = DEFAULT_RELATION_NAME, + ) -> None: + super().__init__(charm, relation_name) + self._charm = charm + self._relation_name = relation_name + self._client_config = client_config + events = self._charm.on[relation_name] + self.framework.observe(events.relation_created, self._on_relation_created_event) + self.framework.observe(events.relation_changed, self._on_relation_changed_event) + self.framework.observe(events.relation_broken, self._on_relation_broken_event) + + def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: + try: + self._update_relation_data(self._client_config, event.relation.id) + except ClientConfigError as e: + self.on.invalid_client_config.emit(e.args[0]) + + def _on_relation_broken_event(self, event: RelationBrokenEvent) -> None: + # Workaround for https://github.com/canonical/operator/issues/888 + self._pop_relation_data(event.relation.id) + if self.is_client_created(): + event.defer() + logger.info("Relation data still available. Deferring the event") + return + + # Notify the requirer that the relation data was removed + self.on.oauth_info_removed.emit() + + def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: + if not self.model.unit.is_leader(): + return + + data = event.relation.data[event.app] + if not data: + logger.info("No relation data available.") + return + + data = _load_data(data, OAUTH_PROVIDER_JSON_SCHEMA) + + client_id = data.get("client_id") + client_secret_id = data.get("client_secret_id") + if not client_id or not client_secret_id: + logger.info("OAuth Provider info is available, waiting for client to be registered.") + # The client credentials are not ready yet, so we do nothing + # This could mean that the client credentials were removed from the databag, + # but we don't allow that (for now), so we don't have to check for it. + return + + self.on.oauth_info_changed.emit(client_id, client_secret_id) + + def _update_relation_data( + self, client_config: Optional[ClientConfig], relation_id: Optional[int] = None + ) -> None: + if not self.model.unit.is_leader() or not client_config: + return + + if not isinstance(client_config, ClientConfig): + raise ValueError(f"Unexpected client_config type: {type(client_config)}") + + client_config.validate() + + try: + relation = self.model.get_relation( + relation_name=self._relation_name, relation_id=relation_id + ) + except TooManyRelatedAppsError: + raise RuntimeError("More than one relations are defined. Please provide a relation_id") + + if not relation or not relation.app: + return + + data = _dump_data(client_config.to_dict(), OAUTH_REQUIRER_JSON_SCHEMA) + relation.data[self.model.app].update(data) + + def is_client_created(self, relation_id: Optional[int] = None) -> bool: + """Check if the client has been created.""" + if len(self.model.relations) == 0: + return None + try: + relation = self.model.get_relation(self._relation_name, relation_id=relation_id) + except TooManyRelatedAppsError: + raise RuntimeError("More than one relations are defined. Please provide a relation_id") + + if not relation or not relation.app: + return None + + return ( + "client_id" in relation.data[relation.app] + and "client_secret_id" in relation.data[relation.app] + ) + + def get_provider_info(self, relation_id: Optional[int] = None) -> OauthProviderConfig: + """Get the provider information from the databag.""" + if len(self.model.relations) == 0: + return None + try: + relation = self.model.get_relation(self._relation_name, relation_id=relation_id) + except TooManyRelatedAppsError: + raise RuntimeError("More than one relations are defined. Please provide a relation_id") + if not relation or not relation.app: + return None + + data = relation.data[relation.app] + if not data: + logger.info("No relation data available.") + return + + data = _load_data(data, OAUTH_PROVIDER_JSON_SCHEMA) + + client_secret_id = data.get("client_secret_id") + if client_secret_id: + _client_secret = self.get_client_secret(client_secret_id) + client_secret = _client_secret.get_content()[CLIENT_SECRET_FIELD] + data["client_secret"] = client_secret + + oauth_provider = OauthProviderConfig.from_dict(data) + return oauth_provider + + def get_client_secret(self, client_secret_id: str) -> Secret: + """Get the client_secret.""" + client_secret = self.model.get_secret(id=client_secret_id) + return client_secret + + def update_client_config( + self, client_config: ClientConfig, relation_id: Optional[int] = None + ) -> None: + """Update the client config stored in the object.""" + self._client_config = client_config + self._update_relation_data(client_config, relation_id=relation_id) + + +class ClientCreatedEvent(EventBase): + """Event to notify the Provider charm to create a new client.""" + + def __init__( + self, + handle: Handle, + redirect_uri: str, + scope: str, + grant_types: List[str], + audience: List, + token_endpoint_auth_method: str, + relation_id: int, + ) -> None: + super().__init__(handle) + self.redirect_uri = redirect_uri + self.scope = scope + self.grant_types = grant_types + self.audience = audience + self.token_endpoint_auth_method = token_endpoint_auth_method + self.relation_id = relation_id + + def snapshot(self) -> Dict: + """Save event.""" + return { + "redirect_uri": self.redirect_uri, + "scope": self.scope, + "grant_types": self.grant_types, + "audience": self.audience, + "token_endpoint_auth_method": self.token_endpoint_auth_method, + "relation_id": self.relation_id, + } + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + self.redirect_uri = snapshot["redirect_uri"] + self.scope = snapshot["scope"] + self.grant_types = snapshot["grant_types"] + self.audience = snapshot["audience"] + self.token_endpoint_auth_method = snapshot["token_endpoint_auth_method"] + self.relation_id = snapshot["relation_id"] + + def to_client_config(self) -> ClientConfig: + """Convert the event information to a ClientConfig object.""" + return ClientConfig( + self.redirect_uri, + self.scope, + self.grant_types, + self.audience, + self.token_endpoint_auth_method, + ) + + +class ClientChangedEvent(EventBase): + """Event to notify the Provider charm that the client config changed.""" + + def __init__( + self, + handle: Handle, + redirect_uri: str, + scope: str, + grant_types: List, + audience: List, + token_endpoint_auth_method: str, + relation_id: int, + client_id: str, + ) -> None: + super().__init__(handle) + self.redirect_uri = redirect_uri + self.scope = scope + self.grant_types = grant_types + self.audience = audience + self.token_endpoint_auth_method = token_endpoint_auth_method + self.relation_id = relation_id + self.client_id = client_id + + def snapshot(self) -> Dict: + """Save event.""" + return { + "redirect_uri": self.redirect_uri, + "scope": self.scope, + "grant_types": self.grant_types, + "audience": self.audience, + "token_endpoint_auth_method": self.token_endpoint_auth_method, + "relation_id": self.relation_id, + "client_id": self.client_id, + } + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + self.redirect_uri = snapshot["redirect_uri"] + self.scope = snapshot["scope"] + self.grant_types = snapshot["grant_types"] + self.audience = snapshot["audience"] + self.token_endpoint_auth_method = snapshot["token_endpoint_auth_method"] + self.relation_id = snapshot["relation_id"] + self.client_id = snapshot["client_id"] + + def to_client_config(self) -> ClientConfig: + """Convert the event information to a ClientConfig object.""" + return ClientConfig( + self.redirect_uri, + self.scope, + self.grant_types, + self.audience, + self.token_endpoint_auth_method, + self.client_id, + ) + + +class ClientDeletedEvent(EventBase): + """Event to notify the Provider charm that the client was deleted.""" + + def __init__( + self, + handle: Handle, + relation_id: int, + ) -> None: + super().__init__(handle) + self.relation_id = relation_id + + def snapshot(self) -> Dict: + """Save event.""" + return {"relation_id": self.relation_id} + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + self.relation_id = snapshot["relation_id"] + + +class OAuthProviderEvents(ObjectEvents): + """Event descriptor for events raised by `OAuthProviderEvents`.""" + + client_created = EventSource(ClientCreatedEvent) + client_changed = EventSource(ClientChangedEvent) + client_deleted = EventSource(ClientDeletedEvent) + + +class OAuthProvider(OAuthRelation): + """A provider object for OIDC Providers.""" + + on = OAuthProviderEvents() + + def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME) -> None: + super().__init__(charm, relation_name) + self._charm = charm + self._relation_name = relation_name + + events = self._charm.on[relation_name] + self.framework.observe( + events.relation_changed, + self._get_client_config_from_relation_data, + ) + self.framework.observe( + events.relation_departed, + self._on_relation_departed, + ) + + def _get_client_config_from_relation_data(self, event: RelationChangedEvent) -> None: + if not self.model.unit.is_leader(): + return + + data = event.relation.data[event.app] + if not data: + logger.info("No requirer relation data available.") + return + + client_data = _load_data(data, OAUTH_REQUIRER_JSON_SCHEMA) + redirect_uri = client_data.get("redirect_uri") + scope = client_data.get("scope") + grant_types = client_data.get("grant_types") + audience = client_data.get("audience") + token_endpoint_auth_method = client_data.get("token_endpoint_auth_method") + + data = event.relation.data[self._charm.app] + if not data: + logger.info("No provider relation data available.") + return + provider_data = _load_data(data, OAUTH_PROVIDER_JSON_SCHEMA) + client_id = provider_data.get("client_id") + + relation_id = event.relation.id + + if client_id: + # Modify an existing client + self.on.client_changed.emit( + redirect_uri, + scope, + grant_types, + audience, + token_endpoint_auth_method, + relation_id, + client_id, + ) + else: + # Create a new client + self.on.client_created.emit( + redirect_uri, scope, grant_types, audience, token_endpoint_auth_method, relation_id + ) + + def _get_secret_label(self, relation: Relation) -> str: + return f"client_secret_{relation.id}" + + def _on_relation_departed(self, event: RelationDepartedEvent) -> None: + # Workaround for https://github.com/canonical/operator/issues/888 + self._pop_relation_data(event.relation.id) + + self._delete_juju_secret(event.relation) + self.on.client_deleted.emit(event.relation.id) + + def _create_juju_secret(self, client_secret: str, relation: Relation) -> Secret: + """Create a juju secret and grant it to a relation.""" + secret = {CLIENT_SECRET_FIELD: client_secret} + juju_secret = self.model.app.add_secret(secret, label=self._get_secret_label(relation)) + juju_secret.grant(relation) + return juju_secret + + def _delete_juju_secret(self, relation: Relation) -> None: + secret = self.model.get_secret(label=self._get_secret_label(relation)) + secret.remove_all_revisions() + + def set_provider_info_in_relation_data( + self, + issuer_url: str, + authorization_endpoint: str, + token_endpoint: str, + introspection_endpoint: str, + userinfo_endpoint: str, + jwks_endpoint: str, + scope: str, + groups: Optional[str] = None, + ca_chain: Optional[str] = None, + ) -> None: + """Put the provider information in the databag.""" + if not self.model.unit.is_leader(): + return + + data = { + "issuer_url": issuer_url, + "authorization_endpoint": authorization_endpoint, + "token_endpoint": token_endpoint, + "introspection_endpoint": introspection_endpoint, + "userinfo_endpoint": userinfo_endpoint, + "jwks_endpoint": jwks_endpoint, + "scope": scope, + } + if groups: + data["groups"] = groups + if ca_chain: + data["ca_chain"] = ca_chain + + for relation in self.model.relations[self._relation_name]: + relation.data[self.model.app].update(_dump_data(data)) + + def set_client_credentials_in_relation_data( + self, relation_id: int, client_id: str, client_secret: str + ) -> None: + """Put the client credentials in the databag.""" + if not self.model.unit.is_leader(): + return + + relation = self.model.get_relation(self._relation_name, relation_id) + if not relation or not relation.app: + return + # TODO: What if we are refreshing the client_secret? We need to add a + # new revision for that + secret = self._create_juju_secret(client_secret, relation) + data = dict(client_id=client_id, client_secret_id=secret.id) + relation.data[self.model.app].update(_dump_data(data)) diff --git a/charms/jimm/metadata.yaml b/charms/jimm/metadata.yaml index 19d492d00..f47f4a5b1 100644 --- a/charms/jimm/metadata.yaml +++ b/charms/jimm/metadata.yaml @@ -34,6 +34,7 @@ provides: cos-agent: interface: cos_agent limit: 1 + requires: database: @@ -43,6 +44,9 @@ requires: optional: true openfga: interface: openfga + oauth: + interface: oauth + limit: 1 resources: jimm-snap: diff --git a/charms/jimm/requirements.txt b/charms/jimm/requirements.txt index 37cf25e8f..d9c19d8ea 100644 --- a/charms/jimm/requirements.txt +++ b/charms/jimm/requirements.txt @@ -5,3 +5,4 @@ charmhelpers >= 0.20.22 hvac >= 0.11.0 pydantic == 1.10.* cosl +jsonschema \ No newline at end of file diff --git a/charms/jimm/src/charm.py b/charms/jimm/src/charm.py index 29427fc5f..0ed65781f 100755 --- a/charms/jimm/src/charm.py +++ b/charms/jimm/src/charm.py @@ -11,6 +11,7 @@ import socket import subprocess import urllib +from urllib.parse import urljoin import hvac from charmhelpers.contrib.charmsupport.nrpe import NRPE @@ -19,6 +20,7 @@ DatabaseRequiresEvent, ) from charms.grafana_agent.v0.cos_agent import COSAgentProvider +from charms.hydra.v0.oauth import ClientConfig, OAuthInfoChangedEvent, OAuthRequirer from charms.openfga_k8s.v0.openfga import OpenFGARequires, OpenFGAStoreCreateEvent from jinja2 import Environment, FileSystemLoader from ops.main import main @@ -28,7 +30,6 @@ MaintenanceStatus, ModelError, Relation, - WaitingStatus, ) from systemd import SystemdCharm @@ -37,6 +38,17 @@ DATABASE_NAME = "jimm" OPENFGA_STORE_NAME = "jimm" +OAUTH = "oauth" +OAUTH_SCOPES = "openid email offline_access" +# TODO: Add "device_code" below once the charm interface supports it. +OAUTH_GRANT_TYPES = ["authorization_code", "refresh_token"] + +# Env file parts +DB_PART = "db" +VAULT_PART = "vault" +OAUTH_PART = "oauth" +LEADER_PART = "leader" +OPENFGA_PART = "openfga" class JimmCharm(SystemdCharm): @@ -83,6 +95,10 @@ def __init__(self, *args): self._on_openfga_store_created, ) + self.oauth = OAuthRequirer(self, self._oauth_client_config, relation_name=OAUTH) + self.framework.observe(self.oauth.on.oauth_info_changed, self._on_oauth_info_changed) + self.framework.observe(self.oauth.on.oauth_info_removed, self._on_oauth_info_removed) + # Grafana agent relation self._grafana_agent = COSAgentProvider( self, @@ -136,8 +152,11 @@ def _on_config_changed(self, _): "audit_retention_period": self.config.get("audit-log-retention-period-in-days", ""), "jwt_expiry": self.config.get("jwt-expiry", "5m"), "macaroon_expiry_duration": self.config.get("macaroon-expiry-duration"), + "session_expiry_duration": self.config.get("session-expiry-duration"), } + self.oauth.update_client_config(client_config=self._oauth_client_config) + if self.config.get("postgres-secret-storage", False): args["insecure_secret_storage"] = "enabled" # Value doesn't matter, only checks env var exists. @@ -159,7 +178,7 @@ def _on_leader_elected(self, _): if self.model.unit.is_leader(): args["jimm_watch_controllers"] = "1" args["jimm_enable_jwks_rotator"] = "1" - with open(self._env_filename("leader"), "wt") as f: + with open(self._env_filename(LEADER_PART), "wt") as f: f.write(self._render_template("jimm-leader.env", **args)) if self._ready(): self.restart() @@ -167,7 +186,6 @@ def _on_leader_elected(self, _): def _on_database_event(self, event: DatabaseRequiresEvent): """Handle database event""" - if not event.endpoints: logger.info("received empty database host address") event.defer() @@ -188,7 +206,7 @@ def _on_database_event(self, event: DatabaseRequiresEvent): logger.info("received database uri: {}".format(uri)) args = {"dsn": uri} - with open(self._env_filename("db"), "wt") as f: + with open(self._env_filename(DB_PART), "wt") as f: f.write(self._render_template("jimm-db.env", **args)) if self._ready(): self.restart() @@ -196,13 +214,40 @@ def _on_database_event(self, event: DatabaseRequiresEvent): def _on_database_relation_broken(self, event) -> None: """Database relation broken handler.""" - if not self._ready(): - event.defer() - logger.warning("Unit is not ready") - return logger.info("database relation removed") + try: + os.remove(self._env_filename(DB_PART)) + except OSError: + pass + self.stop() self._on_update_status(None) + def _on_oauth_info_changed(self, event: OAuthInfoChangedEvent): + if not self.oauth.is_client_created(): + logger.warning("OAuth relation is not ready yet") + return + oauth_provider_info = self.oauth.get_provider_info() + oauth_info = { + "issuer_url": oauth_provider_info.issuer_url, + "client_id": oauth_provider_info.client_id, + "client_secret": oauth_provider_info.client_secret, + "scope": oauth_provider_info.scope, + } + with open(self._env_filename(OAUTH_PART), "wt") as f: + f.write(self._render_template("jimm-oauth.env", **oauth_info)) + if self._ready(): + self.restart() + self._on_update_status(event) + + def _on_oauth_info_removed(self, event: OAuthInfoChangedEvent): + logger.info("oauth relation removed") + try: + os.remove(self._env_filename(OAUTH_PART)) + except OSError: + pass + self.stop() + self._on_update_status(event) + def _on_stop(self, _): """Stop the JIMM service.""" self.stop() @@ -212,14 +257,7 @@ def _on_stop(self, _): def _on_update_status(self, _): """Update the status of the charm.""" - if not os.path.exists(self._workload_filename): - self.unit.status = BlockedStatus("waiting for jimm-snap resource") - return - if not self.model.get_relation("database"): - self.unit.status = BlockedStatus("waiting for database") - return - if not os.path.exists(self._env_filename("db")): - self.unit.status = WaitingStatus("waiting for database") + if not self._ready(): return try: url = "http://localhost:8080/debug/info" @@ -283,7 +321,7 @@ def _on_vault_relation_changed(self, event): "vault_auth_path": "/auth/approle/login", "vault_path": "charm-jimm-creds", } - with open(self._env_filename("vault"), "wt") as f: + with open(self._env_filename(VAULT_PART), "wt") as f: f.write(self._render_template("jimm-vault.env", **args)) def _install_snap(self): @@ -333,10 +371,10 @@ def _bakery_agent_file(self): def _write_service_file(self): args = { "conf_file": self._env_filename(), - "db_file": self._env_filename("db"), - "leader_file": self._env_filename("leader"), - "vault_file": self._env_filename("vault"), - "openfga_file": self._env_filename("openfga"), + "db_file": self._env_filename(DB_PART), + "leader_file": self._env_filename(LEADER_PART), + "vault_file": self._env_filename(VAULT_PART), + "openfga_file": self._env_filename(OPENFGA_PART), } with open(self.service_file, "wt") as f: f.write(self._render_template("jimm.service", **args)) @@ -349,8 +387,20 @@ def _render_template(self, name, **kwargs): def _ready(self): if not os.path.exists(self._env_filename()): + logger.warning("Missing base environment file") + self.unit.status = BlockedStatus("Waiting for environment") + return False + if not os.path.exists(self._env_filename(DB_PART)): + logger.warning("Missing database environment file") + self.unit.status = BlockedStatus("Waiting for database relation") + return False + if not os.path.exists(self._env_filename(OAUTH_PART)): + logger.warning("Missing oauth environment file") + self.unit.status = BlockedStatus("Waiting for oauth relation") return False - if not os.path.exists(self._env_filename("db")): + if not os.path.exists(self._env_filename(OPENFGA_PART)): + logger.warning("Missing openfga environment file") + self.unit.status = BlockedStatus("Waiting for openfga relation") return False return True @@ -400,8 +450,30 @@ def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): "openfga_token": token, } - with open(self._env_filename("openfga"), "wt") as f: + with open(self._env_filename(OPENFGA_PART), "wt") as f: f.write(self._render_template("jimm-openfga.env", **args)) + if self._ready(): + self.restart() + self._on_update_status(None) + + @property + def _oauth_client_config(self) -> ClientConfig: + dns = self.config.get("dns-name") + if dns is None or dns == "": + dns = "http://localhost" + dns = ensureFQDN(dns) + return ClientConfig( + urljoin(dns, "/oauth/callback"), + OAUTH_SCOPES, + OAUTH_GRANT_TYPES, + ) + + +def ensureFQDN(dns: str): # noqa: N802 + """Ensures a domain name has an https:// prefix.""" + if not dns.startswith("http"): + dns = "https://" + dns + return dns def _json_data(event, key): diff --git a/charms/jimm/templates/jimm-oauth.env b/charms/jimm/templates/jimm-oauth.env new file mode 100644 index 000000000..9e6be84bf --- /dev/null +++ b/charms/jimm/templates/jimm-oauth.env @@ -0,0 +1,4 @@ +JIMM_OAUTH_ISSUER_URL={{issuer_url}} +JIMM_OAUTH_CLIENT_ID={{client_id}} +JIMM_OAUTH_CLIENT_SECRET={{client_secret}} +JIMM_OAUTH_SCOPES={{scope}} diff --git a/charms/jimm/templates/jimm.env b/charms/jimm/templates/jimm.env index 59a0ae808..4dc0a4ad5 100644 --- a/charms/jimm/templates/jimm.env +++ b/charms/jimm/templates/jimm.env @@ -23,3 +23,4 @@ INSECURE_SECRET_STORAGE=enabled JIMM_JWT_EXPIRY={{jwt_expiry}} {% endif %} JIMM_MACAROON_EXPIRY_DURATION={{macaroon_expiry_duration}} +JIMM_ACCESS_TOKEN_EXPIRY_DURATION={{session_expiry_duration}} diff --git a/charms/jimm/tests/test_charm.py b/charms/jimm/tests/test_charm.py index c6f97a7dd..c4be19d43 100644 --- a/charms/jimm/tests/test_charm.py +++ b/charms/jimm/tests/test_charm.py @@ -16,11 +16,31 @@ from unittest.mock import MagicMock, Mock, call, patch import hvac -from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus +from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus from ops.testing import Harness from src.charm import JimmCharm +OAUTH_CLIENT_ID = "jimm_client_id" +OAUTH_CLIENT_SECRET = "test-secret" +OAUTH_PROVIDER_INFO = { + "authorization_endpoint": "https://example.oidc.com/oauth2/auth", + "introspection_endpoint": "https://example.oidc.com/admin/oauth2/introspect", + "issuer_url": "https://example.oidc.com", + "jwks_endpoint": "https://example.oidc.com/.well-known/jwks.json", + "scope": "openid profile email phone", + "token_endpoint": "https://example.oidc.com/oauth2/token", + "userinfo_endpoint": "https://example.oidc.com/userinfo", +} + +OPENFGA_PROVIDER_INFO = { + "address": "openfga.localhost", + "port": "8080", + "scheme": "http", + "store_id": "fake-store-id", + "token": "fake-token", +} + class TestCharm(unittest.TestCase): def setUp(self): @@ -45,6 +65,32 @@ def setUp(self): ) self.harness.charm.framework.charm_dir = pathlib.Path(self.tempdir.name) + def add_oauth_relation(self): + self.oauth_rel_id = self.harness.add_relation("oauth", "hydra") + self.harness.add_relation_unit(self.oauth_rel_id, "hydra/0") + secret_id = self.harness.add_model_secret("hydra", {"secret": OAUTH_CLIENT_SECRET}) + self.harness.grant_secret(secret_id, "juju-jimm") + self.harness.update_relation_data( + self.oauth_rel_id, + "hydra", + { + "client_id": OAUTH_CLIENT_ID, + "client_secret_id": secret_id, + **OAUTH_PROVIDER_INFO, + }, + ) + + def add_openfga_relation(self): + self.openfga_rel_id = self.harness.add_relation("openfga", "openfga") + self.harness.add_relation_unit(self.openfga_rel_id, "openfga/0") + self.harness.update_relation_data( + self.openfga_rel_id, + "openfga", + { + **OPENFGA_PROVIDER_INFO, + }, + ) + def test_install(self): service_file = os.path.join(self.harness.charm.charm_dir, "juju-jimm.service") self.harness.add_resource("jimm-snap", "Test data") @@ -66,6 +112,9 @@ def test_start_ready(self): f.write("test") with open(self.harness.charm._env_filename("db"), "wt") as f: f.write("test") + self.harness.set_leader(True) + self.add_oauth_relation() + self.add_openfga_relation() self.harness.charm.on.start.emit() self.harness.charm._systemctl.assert_has_calls( ( @@ -94,6 +143,9 @@ def test_upgrade_charm_ready(self): f.write("test") with open(self.harness.charm._env_filename("db"), "wt") as f: f.write("test") + self.harness.set_leader(True) + self.add_oauth_relation() + self.add_openfga_relation() self.harness.charm.on.upgrade_charm.emit() self.assertTrue(os.path.exists(service_file)) self.assertEqual(self.harness.charm._snap.call_args.args[0], "install") @@ -126,7 +178,7 @@ def test_config_changed(self): with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 21) + self.assertEqual(len(lines), 22) self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=") self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com") self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1") @@ -154,6 +206,7 @@ def test_config_changed(self): "JIMM_JWT_EXPIRY=10m", ) self.assertEqual(lines[20].strip(), "JIMM_MACAROON_EXPIRY_DURATION=48h") + self.assertEqual(lines[21].strip(), "JIMM_ACCESS_TOKEN_EXPIRY_DURATION=6h") def test_config_changed_redirect_to_dashboard(self): config_file = os.path.join(self.harness.charm.charm_dir, "juju-jimm.env") @@ -175,7 +228,7 @@ def test_config_changed_redirect_to_dashboard(self): with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 21) + self.assertEqual(len(lines), 22) self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=") self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com") self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1") @@ -223,7 +276,7 @@ def test_config_changed_ready(self): with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 19) + self.assertEqual(len(lines), 20) self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=") self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com") self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1") @@ -276,7 +329,7 @@ def test_config_changed_with_agent(self): with open(config_file) as f: lines = f.readlines() - self.assertEqual(len(lines), 19) + self.assertEqual(len(lines), 20) self.assertEqual( lines[0].strip(), "BAKERY_AGENT_FILE=" + self.harness.charm._agent_filename, @@ -303,7 +356,7 @@ def test_config_changed_with_agent(self): ) with open(config_file) as f: lines = f.readlines() - self.assertEqual(len(lines), 19) + self.assertEqual(len(lines), 20) self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=") self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com") self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1") @@ -336,6 +389,8 @@ def test_leader_elected_ready(self): lines = f.readlines() self.assertEqual(lines[0].strip(), "JIMM_WATCH_CONTROLLERS=") self.harness.set_leader(True) + self.add_oauth_relation() + self.add_openfga_relation() with open(leader_file) as f: lines = f.readlines() self.assertEqual(lines[0].strip(), "JIMM_WATCH_CONTROLLERS=1") @@ -373,6 +428,9 @@ def test_database_relation_changed_ready(self): db_file = os.path.join(self.harness.charm.charm_dir, "juju-jimm-db.env") with open(self.harness.charm._env_filename(), "wt") as f: f.write("test") + self.harness.set_leader(True) + self.add_oauth_relation() + self.add_openfga_relation() id = self.harness.add_relation("database", "postgresql") self.harness.add_relation_unit(id, "postgresql/0") self.harness.update_relation_data( @@ -476,22 +534,17 @@ def test_update_status(self): self.harness.charm.on.update_status.emit() self.assertEqual( self.harness.charm.unit.status, - BlockedStatus("waiting for jimm-snap resource"), + BlockedStatus("Waiting for environment"), ) - with open(self.harness.charm._workload_filename, "wt") as f: - f.write("jimm.bin") + with open(self.harness.charm._env_filename(), "wt") as f: + f.write("test") self.harness.charm.on.update_status.emit() self.assertEqual( self.harness.charm.unit.status, - BlockedStatus("waiting for database"), + BlockedStatus("Waiting for database relation"), ) id = self.harness.add_relation("database", "postgresql") self.harness.add_relation_unit(id, "postgresql/0") - self.harness.charm.on.update_status.emit() - self.assertEqual( - self.harness.charm.unit.status, - WaitingStatus("waiting for database"), - ) self.harness.update_relation_data( id, "postgresql", @@ -501,7 +554,17 @@ def test_update_status(self): "endpoints": "some.database.host,some.other.database.host", }, ) - self.harness.charm.on.update_status.emit() + self.assertEqual( + self.harness.charm.unit.status, + BlockedStatus("Waiting for oauth relation"), + ) + self.harness.set_leader(True) + self.add_oauth_relation() + self.assertEqual( + self.harness.charm.unit.status, + BlockedStatus("Waiting for openfga relation"), + ) + self.add_openfga_relation() self.assertEqual(self.harness.charm.unit.status, MaintenanceStatus("starting")) s = HTTPServer(("", 8080), VersionHTTPRequestHandler) t = Thread(target=s.serve_forever) @@ -591,14 +654,14 @@ def test_insecure_secret_storage(self): with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 21) + self.assertEqual(len(lines), 22) self.assertEqual(len([match for match in lines if "INSECURE_SECRET_STORAGE" in match]), 0) self.harness.update_config({"postgres-secret-storage": True}) self.assertTrue(os.path.exists(config_file)) with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 23) + self.assertEqual(len(lines), 24) self.assertEqual(len([match for match in lines if "INSECURE_SECRET_STORAGE" in match]), 1) From c7abd029d5929b4b9c5c679ba1ef43570663a8b7 Mon Sep 17 00:00:00 2001 From: alesstimec Date: Thu, 29 Feb 2024 15:05:26 +0100 Subject: [PATCH 069/126] Various device flow fixes. --- docker-compose.yaml | 4 +++- internal/auth/oauth2.go | 1 + internal/jujuapi/admin.go | 3 +-- internal/jujuapi/admin_test.go | 4 ++-- service.go | 9 +++++---- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index f094ebe80..b13446480 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -74,10 +74,11 @@ services: OPENFGA_STORE: "01GP1254CHWJC1MNGVB0WDG1T0" OPENFGA_AUTH_MODEL: "01GP1EC038KHGB6JJ2XXXXCXKB" OPENFGA_TOKEN: "jimm" - JIMM_OAUTH_ISSUER_URL: "http://keycloak:8082/realms/jimm" # Scheme required + JIMM_OAUTH_ISSUER_URL: "http://keycloak.localhost:8082/realms/jimm" # Scheme required JIMM_OAUTH_CLIENT_ID: "jimm-device" JIMM_OAUTH_CLIENT_SECRET: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4" JIMM_OAUTH_SCOPES: "openid profile email" # Space separated list of scopes + JIMM_ACCESS_TOKEN_EXPIRY_DURATION: 1h volumes: - ./:/jimm/ - ./local/vault/approle.json:/vault/approle.json:rw @@ -235,6 +236,7 @@ services: keycloak: image: docker.io/bitnami/keycloak:23 container_name: keycloak + hostname: keycloak.localhost environment: KEYCLOAK_HTTP_PORT: 8082 KEYCLOAK_ENABLE_HEALTH_ENDPOINTS: true diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 4a9fc995f..17dd12c17 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -151,6 +151,7 @@ func (as *AuthenticationService) ExtractAndVerifyIDToken(ctx context.Context, oa token, err := verifier.Verify(ctx, rawIDToken) if err != nil { + zapctx.Error(ctx, "failed to verify id token", zap.Error(err)) return nil, errors.E(op, err, "failed to verify id token") } diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index 50aad4bc1..014206a95 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -96,7 +96,6 @@ func (r *controllerRoot) LoginDevice(ctx context.Context) (params.LoginDeviceRes if err != nil { return response, errors.E(op, err) } - // NOTE: As this is on the controller root struct, and a new controller root // is created per WS, it is EXPECTED that the subsequent call to GetDeviceSessionToken // happens on the SAME websocket. @@ -189,7 +188,7 @@ func (r *controllerRoot) LoginWithSessionToken(ctx context.Context, req params.L if stderrors.As(err, &aerr) { return aerr.LoginResult, nil } - return jujuparams.LoginResult{}, errors.E(op, err) + return jujuparams.LoginResult{}, errors.E(op, err, errors.CodeUnauthorized) } // Get an OpenFGA user to place on the controllerRoot for this WS diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index c2e353a07..28df18ef3 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -142,11 +142,11 @@ func (s *adminSuite) TestDeviceLogin(c *gc.C) { // Test no token present var loginResult jujuparams.LoginResult err = conn.APICall("Admin", 4, "", "LoginWithSessionToken", nil, &loginResult) - c.Assert(err, gc.ErrorMatches, "authentication failed, no token presented") + c.Assert(err, gc.ErrorMatches, "authentication failed, no token presented.*") // Test token not base64 encoded err = conn.APICall("Admin", 4, "", "LoginWithSessionToken", params.LoginWithSessionTokenRequest{SessionToken: string(decodedToken)}, &loginResult) - c.Assert(err, gc.ErrorMatches, "authentication failed, failed to decode token") + c.Assert(err, gc.ErrorMatches, "authentication failed, failed to decode token.*") // Test token base64 encoded passes authentication err = conn.APICall("Admin", 4, "", "LoginWithSessionToken", params.LoginWithSessionTokenRequest{SessionToken: sessionTokenResp.SessionToken}, &loginResult) diff --git a/service.go b/service.go index bcfcaee6c..497460927 100644 --- a/service.go +++ b/service.go @@ -300,10 +300,11 @@ func NewService(ctx context.Context, p Params) (*Service, error) { authSvc, err := auth.NewAuthenticationService( ctx, auth.AuthenticationServiceParams{ - IssuerURL: p.OAuthAuthenticatorParams.IssuerURL, - ClientID: p.OAuthAuthenticatorParams.ClientID, - ClientSecret: p.OAuthAuthenticatorParams.ClientSecret, - Scopes: p.OAuthAuthenticatorParams.Scopes, + IssuerURL: p.OAuthAuthenticatorParams.IssuerURL, + ClientID: p.OAuthAuthenticatorParams.ClientID, + ClientSecret: p.OAuthAuthenticatorParams.ClientSecret, + Scopes: p.OAuthAuthenticatorParams.Scopes, + SessionTokenExpiry: p.OAuthAuthenticatorParams.SessionTokenExpiry, }, ) s.jimm.OAuthAuthenticator = authSvc From cb1c0d36d06e0f448f179453f37be0c64b980104 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:26:35 +0200 Subject: [PATCH 070/126] Change keycloak database backend (#1169) --- docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index f094ebe80..b6891e50d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -241,7 +241,7 @@ services: KEYCLOAK_CREATE_ADMIN_USER: true KEYCLOAK_ADMIN_USER: jimm KEYCLOAK_ADMIN_PASSWORD: jimm - KEYCLOAK_DATABASE_VENDOR: dev-mem + KEYCLOAK_DATABASE_VENDOR: dev-file KEYCLOAK_EXTRA_ARGS: "-Dkeycloak.migration.action=import -Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=/bitnami/keycloak/data/import/realm.json -Dkeycloak.migration.replace-placeholders=true -Dkeycloak.profile.feature.upload_scripts=enabled" volumes: - ./local/keycloak/jimm-realm.json:/bitnami/keycloak/data/import/realm.json:ro From 122abfb5696c681cf28ea3104974c34ed036904a Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Wed, 6 Mar 2024 11:37:19 +0000 Subject: [PATCH 071/126] Css 6646/callback endpoint (#1170) * feat(oauth login browser): implements /auth/login As discussed on call, we integration test only the handler and avoid mocks to see the behaviour is as expected. This is also true when we come to implements the callback logic and are required to start the flow from scratch. The current test in auth_handler_test will be updated to cover the entire flow when implementing /callback. As for the state todo, I need to see how to track the state between requests and the TODO will be completed in the /callback PR. 6646 * pr comments * pr comments * feat(browser flow for dashboard): implements browser flow (without sessions) This PR includes a small refactor to the admin device flow, such that it can share the same identity update logic within the browser flow. * feat(validation in auth handler): validates params given are correct and auth svc not nill * Fix tests * pr comments * additional failure tests * test fix * Update refresh token --- cmd/jimmsrv/main.go | 1 + docker-compose.yaml | 1 + internal/auth/oauth2.go | 66 +++++++++++++ internal/auth/oauth2_test.go | 35 +++++-- internal/cmdtest/jimmsuite.go | 1 + internal/dbmodel/identity.go | 5 + internal/dbmodel/sql/postgres/1_6.sql | 1 + internal/jimm/jimm.go | 4 + internal/jimm/user_test.go | 11 ++- internal/jimmhttp/auth_handler.go | 81 ++++++++++++++-- internal/jimmhttp/auth_handler_test.go | 122 ++++++++++++++++++++++--- internal/jimmjwx/utils_test.go | 1 + internal/jujuapi/admin.go | 28 +----- internal/jujuapi/admin_test.go | 6 ++ service.go | 11 ++- service_test.go | 8 ++ 16 files changed, 322 insertions(+), 60 deletions(-) diff --git a/cmd/jimmsrv/main.go b/cmd/jimmsrv/main.go index db7a665d2..d10ccce26 100644 --- a/cmd/jimmsrv/main.go +++ b/cmd/jimmsrv/main.go @@ -152,6 +152,7 @@ func start(ctx context.Context, s *service.Service) error { Scopes: scopesParsed, SessionTokenExpiry: sessionTokenExpiryDuration, }, + DashboardFinalRedirectURL: os.Getenv("JIMM_DASHBOARD_FINAL_REDIRECT_URL"), }) if err != nil { return err diff --git a/docker-compose.yaml b/docker-compose.yaml index b15a807b4..a74026e50 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -78,6 +78,7 @@ services: JIMM_OAUTH_CLIENT_ID: "jimm-device" JIMM_OAUTH_CLIENT_SECRET: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4" JIMM_OAUTH_SCOPES: "openid profile email" # Space separated list of scopes + JIMM_DASHBOARD_FINAL_REDIRECT_URL: "https://my-dashboard.com/final-callback" # Example URL JIMM_ACCESS_TOKEN_EXPIRY_DURATION: 1h volumes: - ./:/jimm/ diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 17dd12c17..ac90fe244 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -7,6 +7,7 @@ import ( "encoding/base64" stderrors "errors" "net/mail" + "strings" "time" "github.com/coreos/go-oidc/v3/oidc" @@ -16,6 +17,7 @@ import ( "go.uber.org/zap" "golang.org/x/oauth2" + "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" ) @@ -27,6 +29,15 @@ type AuthenticationService struct { provider *oidc.Provider // sessionTokenExpiry holds the expiry time for JIMM minted session tokens (JWTs). sessionTokenExpiry time.Duration + + db IdentityStore +} + +// Identity store holds the necessary methods to get and update an identity +// within JIMM's store. +type IdentityStore interface { + GetIdentity(ctx context.Context, u *dbmodel.Identity) error + UpdateIdentity(ctx context.Context, u *dbmodel.Identity) error } // AuthenticationServiceParams holds the parameters to initialise @@ -48,6 +59,11 @@ type AuthenticationServiceParams struct { // codes into access tokens (and id tokens), for JIMM, this is expected // to be the servers own callback endpoint registered under /auth/callback. RedirectURL string + + // Store holds the identity store used by the authentication service + // to fetch and update identities. I.e., their access tokens, refresh tokens, + // display name, etc. + Store IdentityStore } // NewAuthenticationService returns a new authentication service for handling @@ -71,6 +87,7 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP RedirectURL: params.RedirectURL, }, sessionTokenExpiry: params.SessionTokenExpiry, + db: params.Store, }, nil } @@ -86,6 +103,27 @@ func (as *AuthenticationService) AuthCodeURL() string { return as.oauthConfig.AuthCodeURL("") } +// Exchange exchanges an authorisation code for an access token. +// +// TODO(ale8k): How to test this? A callback has to be made and it needs to be valid, +// this may need some thought as to whether its actually worth testing or are we +// just testing the library. The handler test essentially covers this so perhaps +// its ok to leave it as is? +func (as *AuthenticationService) Exchange(ctx context.Context, code string) (*oauth2.Token, error) { + const op = errors.Op("auth.AuthenticationService.Exchange") + + t, err := as.oauthConfig.Exchange( + ctx, + code, + oauth2.SetAuthURLParam("client_secret", as.oauthConfig.ClientSecret), + ) + if err != nil { + return nil, errors.E(op, err, "authorisation code exchange failed") + } + + return t, nil +} + // Device initiates a device flow login and is step ONE of TWO. // // This is done via retrieving a: @@ -203,6 +241,34 @@ func (as *AuthenticationService) VerifySessionToken(token string, secretKey stri return VerifySessionToken(token, secretKey) } +// UpdateIdentity updates the database with the display name and access token set for the user. +// And, if present, a refresh token. +func (as *AuthenticationService) UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error { + db := as.db + u := &dbmodel.Identity{ + Name: email, + } + // TODO(babakks): If user does not exist, we will create one with an empty + // display name (which we shouldn't). So it would be better to fetch + // and then create. At the moment, GetUser is used for both create and fetch, + // this should be changed and split apart so it is intentional what entities + // we are creating or fetching. + if err := db.GetIdentity(ctx, u); err != nil { + return err + } + // Check if user has a display name, if not, set one + if u.DisplayName == "" { + u.DisplayName = strings.Split(email, "@")[0] + } + u.AccessToken = token.AccessToken + u.RefreshToken = token.RefreshToken + if err := db.UpdateIdentity(ctx, u); err != nil { + return err + } + + return nil +} + // VerifySessionToken symmetrically verifies the validty of the signature on the // access token JWT, returning the parsed token. // diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 37113ee5b..5014c7347 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -14,12 +14,19 @@ import ( "time" "github.com/canonical/jimm/internal/auth" + "github.com/canonical/jimm/internal/db" + "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/jimmtest" "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" ) -func setupTestAuthSvc(ctx context.Context, c *qt.C, expiry time.Duration) *auth.AuthenticationService { +func setupTestAuthSvc(ctx context.Context, c *qt.C, expiry time.Duration) (*auth.AuthenticationService, *db.Database) { + db := &db.Database{ + DB: jimmtest.PostgresDB(c, func() time.Time { return time.Now() }), + } + c.Assert(db.Migrate(ctx, false), qt.IsNil) + authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ IssuerURL: "http://localhost:8082/realms/jimm", ClientID: "jimm-device", @@ -27,10 +34,11 @@ func setupTestAuthSvc(ctx context.Context, c *qt.C, expiry time.Duration) *auth. Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: expiry, RedirectURL: "http://localhost:8080/auth/callback", + Store: db, }) c.Assert(err, qt.IsNil) - return authSvc + return authSvc, db } // This test requires the local docker compose to be running and keycloak @@ -41,7 +49,7 @@ func TestAuthCodeURL(t *testing.T) { c := qt.New(t) ctx := context.Background() - authSvc := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) url := authSvc.AuthCodeURL() c.Assert( @@ -67,7 +75,7 @@ func TestDevice(t *testing.T) { ctx := context.Background() - authSvc := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, db := setupTestAuthSvc(ctx, c, time.Hour) res, err := authSvc.Device(ctx) c.Assert(err, qt.IsNil) @@ -138,6 +146,17 @@ func TestDevice(t *testing.T) { email, err := authSvc.Email(idToken) c.Assert(err, qt.IsNil) c.Assert(email, qt.Equals, u.Email) + + // Update the identity + err = authSvc.UpdateIdentity(ctx, email, token) + c.Assert(err, qt.IsNil) + + updatedUser := &dbmodel.Identity{ + Name: u.Email, + } + c.Assert(db.GetIdentity(ctx, updatedUser), qt.IsNil) + c.Assert(updatedUser.AccessToken, qt.Not(qt.Equals), "") + c.Assert(updatedUser.RefreshToken, qt.Not(qt.Equals), "") } // TestSessionTokens tests both the minting and validation of JIMM @@ -147,7 +166,7 @@ func TestSessionTokens(t *testing.T) { ctx := context.Background() - authSvc := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) secretKey := "secret-key" token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) @@ -164,7 +183,7 @@ func TestSessionTokenRejectsWrongSecretKey(t *testing.T) { ctx := context.Background() - authSvc := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) secretKey := "secret-key" token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) @@ -181,7 +200,7 @@ func TestSessionTokenRejectsExpiredToken(t *testing.T) { ctx := context.Background() noDuration := time.Duration(0) - authSvc := setupTestAuthSvc(ctx, c, noDuration) + authSvc, _ := setupTestAuthSvc(ctx, c, noDuration) secretKey := "secret-key" token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) @@ -197,7 +216,7 @@ func TestSessionTokenValidatesEmail(t *testing.T) { ctx := context.Background() - authSvc := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) secretKey := "secret-key" token, err := authSvc.MintSessionToken("", secretKey) diff --git a/internal/cmdtest/jimmsuite.go b/internal/cmdtest/jimmsuite.go index fdf041d1f..748ae8060 100644 --- a/internal/cmdtest/jimmsuite.go +++ b/internal/cmdtest/jimmsuite.go @@ -91,6 +91,7 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, + DashboardFinalRedirectURL: "", } srv, err := service.NewService(ctx, s.Params) diff --git a/internal/dbmodel/identity.go b/internal/dbmodel/identity.go index 8f6e30e38..df69dd680 100644 --- a/internal/dbmodel/identity.go +++ b/internal/dbmodel/identity.go @@ -39,6 +39,11 @@ type Identity struct { // from the browser or device flow, and as such is updated on every successful // login. AccessToken string + + // RefreshToken is an OAuth2.0 refresh token for this identity, it may have come + // from the browser or device flow, and as such is updated on every successful + // login. + RefreshToken string } // Tag returns a names.Tag for the identity. diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql index 62f17acea..d5ba10f6d 100644 --- a/internal/dbmodel/sql/postgres/1_6.sql +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -1,6 +1,7 @@ -- 1_6.sql is a migration that adds access tokens to the user table -- and is a migration that renames `user` to `identity`. ALTER TABLE users ADD COLUMN access_token TEXT; +ALTER TABLE users ADD COLUMN refresh_token TEXT; -- Note that we don't need to rename underlying indexes/constraints. As Postgres -- docs states: diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index b43182256..9a2a5114d 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -165,6 +165,10 @@ type OAuthAuthenticator interface { // The subject of the token contains the user's email and can be used // for user object creation. VerifySessionToken(token string, secretKey string) (jwt.Token, error) + + // UpdateIdentity updates the database with the display name and access token set for the user. + // And, if present, a refresh token. + UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error } type permission struct { diff --git a/internal/jimm/user_test.go b/internal/jimm/user_test.go index 8ea5467d4..bd4909776 100644 --- a/internal/jimm/user_test.go +++ b/internal/jimm/user_test.go @@ -108,21 +108,22 @@ func TestGetOpenFGAUser(t *testing.T) { client, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) + db := &db.Database{ + DB: jimmtest.PostgresDB(c, func() time.Time { return time.Now() }), + } // TODO(ale8k): Mock this authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ IssuerURL: "http://localhost:8082/realms/jimm", ClientID: "jimm-device", Scopes: []string{"openid", "profile", "email"}, SessionTokenExpiry: time.Hour, + Store: db, }) c.Assert(err, qt.IsNil) - now := time.Now().UTC().Round(time.Millisecond) j := &jimm.JIMM{ - UUID: "test", - Database: db.Database{ - DB: jimmtest.PostgresDB(c, func() time.Time { return now }), - }, + UUID: "test", + Database: *db, OAuthAuthenticator: authSvc, OpenFGAClient: client, } diff --git a/internal/jimmhttp/auth_handler.go b/internal/jimmhttp/auth_handler.go index 4ec601e7f..bcc12af67 100644 --- a/internal/jimmhttp/auth_handler.go +++ b/internal/jimmhttp/auth_handler.go @@ -1,34 +1,56 @@ package jimmhttp import ( + "context" "net/http" + "github.com/coreos/go-oidc/v3/oidc" "github.com/go-chi/chi/v5" + "github.com/juju/zaputil/zapctx" + "go.uber.org/zap" + "golang.org/x/oauth2" + + "github.com/canonical/jimm/internal/errors" ) // OAuthHandler handles the oauth2.0 browser flow for JIMM. // Implements jimmhttp.JIMMHttpHandler. type OAuthHandler struct { - Router *chi.Mux - Authenticator BrowserOAuthAuthenticator + Router *chi.Mux + Authenticator BrowserOAuthAuthenticator + DashboardFinalRedirectURL string } // BrowserOAuthAuthenticator handles authorisation code authentication within JIMM // via OIDC. type BrowserOAuthAuthenticator interface { - // AuthCodeURL returns a URL that will be used to redirect a browser to the identity provider. AuthCodeURL() string + Exchange(ctx context.Context, code string) (*oauth2.Token, error) + ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) + Email(idToken *oidc.IDToken) (string, error) + UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error } // NewOAuthHandler returns a new OAuth handler. -func NewOAuthHandler(authenticator BrowserOAuthAuthenticator) *OAuthHandler { - return &OAuthHandler{Router: chi.NewRouter(), Authenticator: authenticator} +func NewOAuthHandler(authenticator BrowserOAuthAuthenticator, dashboardFinalRedirectURL string) (*OAuthHandler, error) { + if authenticator == nil { + return nil, errors.E("nil authenticator") + } + if dashboardFinalRedirectURL == "" { + return nil, errors.E("final redirect url not specified") + } + return &OAuthHandler{ + Router: chi.NewRouter(), + Authenticator: authenticator, + DashboardFinalRedirectURL: dashboardFinalRedirectURL, + }, nil } // Routes returns the grouped routers routes with group specific middlewares. func (oah *OAuthHandler) Routes() chi.Router { oah.SetupMiddleware() oah.Router.Get("/login", oah.Login) + oah.Router.Get("/callback", oah.Callback) return oah.Router } @@ -36,8 +58,55 @@ func (oah *OAuthHandler) Routes() chi.Router { func (oah *OAuthHandler) SetupMiddleware() { } -// Login handles /auth/login, +// Login handles /auth/login. func (oah *OAuthHandler) Login(w http.ResponseWriter, r *http.Request) { redirectURL := oah.Authenticator.AuthCodeURL() http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect) } + +// Callback handles /auth/callback. +func (oah *OAuthHandler) Callback(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + + code := r.URL.Query().Get("code") + + authSvc := oah.Authenticator + + if code == "" { + writeError(ctx, w, http.StatusBadRequest, nil, "no authorisation code present") + return + } + + token, err := authSvc.Exchange(ctx, code) + if err != nil { + writeError(ctx, w, http.StatusBadRequest, err, "failed to exchange authcode") + return + } + + idToken, err := authSvc.ExtractAndVerifyIDToken(ctx, token) + if err != nil { + writeError(ctx, w, http.StatusBadRequest, err, "failed to extract and verify id token") + return + } + + email, err := authSvc.Email(idToken) + if err != nil { + writeError(ctx, w, http.StatusBadRequest, err, "failed to extract email from id token") + return + } + + if err := authSvc.UpdateIdentity(ctx, email, token); err != nil { + writeError(ctx, w, http.StatusBadRequest, err, "failed to update identity") + return + } + + http.Redirect(w, r, oah.DashboardFinalRedirectURL, http.StatusPermanentRedirect) +} + +// writeError writes an error and logs the message. It is expected that the status code +// is an erroneous status code. +func writeError(ctx context.Context, w http.ResponseWriter, status int, err error, logMessage string) { + zapctx.Error(ctx, logMessage, zap.Error(err)) + w.WriteHeader(status) + w.Write([]byte(http.StatusText(status))) +} diff --git a/internal/jimmhttp/auth_handler_test.go b/internal/jimmhttp/auth_handler_test.go index 186e705da..d274fd8ab 100644 --- a/internal/jimmhttp/auth_handler_test.go +++ b/internal/jimmhttp/auth_handler_test.go @@ -2,22 +2,29 @@ package jimmhttp_test import ( "context" + "fmt" + "io" "math/rand" "net" "net/http" + "net/http/cookiejar" "net/http/httptest" + "net/url" + "regexp" "strconv" "testing" "time" - "github.com/canonical/jimm/internal/auth" - "github.com/canonical/jimm/internal/jimmhttp" - "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" + + "github.com/canonical/jimm/internal/auth" + "github.com/canonical/jimm/internal/db" + "github.com/canonical/jimm/internal/jimmhttp" + "github.com/canonical/jimm/internal/jimmtest" ) -func setupTestServer(c *qt.C) *httptest.Server { +func setupTestServer(c *qt.C, dashboardURL string) *httptest.Server { // Create unstarted server to enable auth service s := httptest.NewUnstartedServer(nil) // Setup random port listener @@ -31,8 +38,11 @@ func setupTestServer(c *qt.C) *httptest.Server { s.Listener = l // Remember redirect url to check it matches after test server starts - redirectURL := "http://127.0.0.1:" + port + "/auth/callback" - + redirectURL := "http://127.0.0.1:" + port + "/callback" + db := &db.Database{ + DB: jimmtest.PostgresDB(c, func() time.Time { return time.Now() }), + } + c.Assert(db.Migrate(context.Background(), false), qt.IsNil) authSvc, err := auth.NewAuthenticationService(context.Background(), auth.AuthenticationServiceParams{ IssuerURL: "http://localhost:8082/realms/jimm", ClientID: "jimm-device", @@ -41,28 +51,114 @@ func setupTestServer(c *qt.C) *httptest.Server { SessionTokenExpiry: time.Hour, // Now we know the port the test server is running on RedirectURL: redirectURL, + Store: db, }) c.Assert(err, qt.IsNil) - r := jimmhttp.NewOAuthHandler(authSvc).Routes() - s.Config.Handler = r + h, err := jimmhttp.NewOAuthHandler(authSvc, dashboardURL) + c.Assert(err, qt.IsNil) + + s.Config.Handler = h.Routes() s.Start() // Ensure redirectURL is matching port on listener - c.Assert(s.URL+"/auth/callback", qt.Equals, redirectURL) + c.Assert(s.URL+"/callback", qt.Equals, redirectURL) return s } -// Login is simply expected to redirect the user -func TestAuthLogin(t *testing.T) { +// TestBrowserAuth goes through the flow of a browser logging in, simulating +// the cookie state and handling the callbacks are as expected. Additionally handling +// the final callback to the dashboard emulating an endpoint. See setupTestServer +// where we create an additional handler to simulate the final callback to the dashboard +// from JIMM. +func TestBrowserAuth(t *testing.T) { c := qt.New(t) - s := setupTestServer(c) + // Setup final test redirect url server, to emulate + // the dashboard receiving the final piece of the flow + dashboardResponse := "dashboard received final callback" + dashboard := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, dashboardResponse) + }, + ), + ) + defer dashboard.Close() + + s := setupTestServer(c, dashboard.URL) defer s.Close() - res, err := http.Get(s.URL + "/login") + jar, err := cookiejar.New(nil) + c.Assert(err, qt.IsNil) + + client := &http.Client{ + Jar: jar, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + fmt.Println("redirected to", req.URL) + return nil + }, + } + + res, err := client.Get(s.URL + "/login") c.Assert(err, qt.IsNil) c.Assert(res.StatusCode, qt.Equals, http.StatusOK) + + defer res.Body.Close() + b, err := io.ReadAll(res.Body) + c.Assert(err, qt.IsNil) + + re := regexp.MustCompile(`action="(.*?)" method=`) + match := re.FindStringSubmatch(string(b)) + loginFormUrl := match[1] + + v := url.Values{} + v.Add("username", "jimm-test") + v.Add("password", "password") + loginResp, err := client.PostForm(loginFormUrl, v) + c.Assert(err, qt.IsNil) + + b, err = io.ReadAll(loginResp.Body) + c.Assert(err, qt.IsNil) + + c.Assert(string(b), qt.Equals, dashboardResponse) + c.Assert(loginResp.StatusCode, qt.Equals, 200) + + defer loginResp.Body.Close() +} + +func TestCallbackFailsNoCodePresent(t *testing.T) { + c := qt.New(t) + + s := setupTestServer(c, "") + defer s.Close() + + // Test with no code present at all + res, err := http.Get(s.URL + "/callback") + c.Assert(err, qt.IsNil) + + defer res.Body.Close() + + b, err := io.ReadAll(res.Body) + c.Assert(err, qt.IsNil) + c.Assert(string(b), qt.Equals, http.StatusText(http.StatusBadRequest)) +} + +func TestCallbackFailsExchange(t *testing.T) { + c := qt.New(t) + + s := setupTestServer(c, "") + defer s.Close() + + // Test with no code present at all + res, err := http.Get(s.URL + "/callback?code=idonotexist") + c.Assert(err, qt.IsNil) + + defer res.Body.Close() + + b, err := io.ReadAll(res.Body) + c.Assert(err, qt.IsNil) + c.Assert(string(b), qt.Equals, http.StatusText(http.StatusBadRequest)) } diff --git a/internal/jimmjwx/utils_test.go b/internal/jimmjwx/utils_test.go index e0a6d47f1..642a776b7 100644 --- a/internal/jimmjwx/utils_test.go +++ b/internal/jimmjwx/utils_test.go @@ -114,6 +114,7 @@ func setupService(ctx context.Context, c *qt.C) (*jimm.Service, *httptest.Server Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, + DashboardFinalRedirectURL: "", }) c.Assert(err, qt.IsNil) diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index 014206a95..a0d6e010c 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -6,7 +6,6 @@ import ( "context" stderrors "errors" "sort" - "strings" "github.com/juju/juju/rpc" jujuparams "github.com/juju/juju/rpc/params" @@ -14,7 +13,6 @@ import ( "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/auth" - "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/openfga" "github.com/canonical/jimm/internal/servermon" @@ -132,33 +130,9 @@ func (r *controllerRoot) GetDeviceSessionToken(ctx context.Context) (params.GetD return response, errors.E(op, err) } - // TODO(ale8k): Move this into a service, don't do db logic - // at the handler level - // Now we know who the user is, i.e., their email - // we'll update their access token. - // - // Build username + display name - db := r.jimm.DB() - u := &dbmodel.Identity{ - Name: email, - } - // TODO(babakks): If user does not exist, we will create one with an empty - // display name (which we shouldn't). So it would be better to fetch - // and then create. At the moment, GetUser is used for both create and fetch, - // this should be changed and split apart so it is intentional what entities - // we are creating or fetching. - if err := db.GetIdentity(ctx, u); err != nil { - return response, errors.E(op, err) - } - // Check if user has a display name, if not, set one - if u.DisplayName == "" { - u.DisplayName = strings.Split(email, "@")[0] - } - u.AccessToken = token.AccessToken - if err := r.jimm.DB().UpdateIdentity(ctx, u); err != nil { + if err := authSvc.UpdateIdentity(ctx, email, token); err != nil { return response, errors.E(op, err) } - // // TODO(ale8k): Add vault logic to get secret key and generate one // on start up. diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index 28df18ef3..9869ca567 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -18,6 +18,7 @@ import ( "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/jimmtest" + "github.com/coreos/go-oidc/v3/oidc" "github.com/juju/juju/api" jujuparams "github.com/juju/juju/rpc/params" @@ -32,6 +33,7 @@ type adminSuite struct { func (s *adminSuite) SetUpTest(c *gc.C) { s.websocketSuite.SetUpTest(c) ctx := context.Background() + // Replace JIMM's mock authenticator with a real one here // for testing the login flows. authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ @@ -40,6 +42,7 @@ func (s *adminSuite) SetUpTest(c *gc.C) { ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Hour, + Store: &s.JIMM.Database, }) c.Assert(err, gc.Equals, nil) s.JIMM.OAuthAuthenticator = authSvc @@ -79,6 +82,9 @@ func (s *adminSuite) TestDeviceLogin(c *gc.C) { }, "test") defer conn.Close() + err := s.JIMM.Database.Migrate(context.Background(), false) + c.Assert(err, gc.IsNil) + // Create a user in keycloak user, err := jimmtest.CreateRandomKeycloakUser() c.Assert(err, gc.IsNil) diff --git a/service.go b/service.go index 497460927..f59fab015 100644 --- a/service.go +++ b/service.go @@ -180,6 +180,10 @@ type Params struct { // OAuthAuthenticatorParams holds parameters needed to configure an OAuthAuthenticator // implementation. OAuthAuthenticatorParams OAuthAuthenticatorParams + + // DashboardFinalRedirectURL is the URL to FINALLY redirect to after completing + // the /callback in an authorisation code OAuth2.0 flow to finish the flow. + DashboardFinalRedirectURL string } // A Service is the implementation of a JIMM server. @@ -305,6 +309,7 @@ func NewService(ctx context.Context, p Params) (*Service, error) { ClientSecret: p.OAuthAuthenticatorParams.ClientSecret, Scopes: p.OAuthAuthenticatorParams.Scopes, SessionTokenExpiry: p.OAuthAuthenticatorParams.SessionTokenExpiry, + Store: &s.jimm.Database, }, ) s.jimm.OAuthAuthenticator = authSvc @@ -351,9 +356,13 @@ func NewService(ctx context.Context, p Params) (*Service, error) { "/.well-known", wellknownapi.NewWellKnownHandler(s.jimm.CredentialStore), ) + oauthHandler, err := jimmhttp.NewOAuthHandler(authSvc, p.DashboardFinalRedirectURL) + if err != nil { + return nil, errors.E(op, err, "failed to setup authentication handler") + } mountHandler( "/auth", - jimmhttp.NewOAuthHandler(authSvc), + oauthHandler, ) params := jujuapi.Params{ diff --git a/service_test.go b/service_test.go index d225acdbc..e019db7ad 100644 --- a/service_test.go +++ b/service_test.go @@ -56,6 +56,7 @@ func TestDefaultService(t *testing.T) { Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, + DashboardFinalRedirectURL: "", }) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() @@ -80,6 +81,7 @@ func TestServiceStartsWithoutSecretStore(t *testing.T) { Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, + DashboardFinalRedirectURL: "", }) c.Assert(err, qt.IsNil) } @@ -102,6 +104,7 @@ func TestAuthenticator(t *testing.T) { Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, + DashboardFinalRedirectURL: "", } candid := startCandid(c, &p) svc, err := jimm.NewService(context.Background(), p) @@ -171,6 +174,7 @@ func TestVault(t *testing.T) { Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, + DashboardFinalRedirectURL: "", } candid := startCandid(c, &p) vaultClient, _, creds, _ := jimmtest.VaultClient(c, ".") @@ -240,6 +244,7 @@ func TestPostgresSecretStore(t *testing.T) { Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, + DashboardFinalRedirectURL: "", } _, err = jimm.NewService(context.Background(), p) c.Assert(err, qt.IsNil) @@ -262,6 +267,7 @@ func TestOpenFGA(t *testing.T) { Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, + DashboardFinalRedirectURL: "", } candid := startCandid(c, &p) svc, err := jimm.NewService(context.Background(), p) @@ -318,6 +324,7 @@ func TestPublicKey(t *testing.T) { Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, + DashboardFinalRedirectURL: "", } _ = startCandid(c, &p) svc, err := jimm.NewService(context.Background(), p) @@ -406,6 +413,7 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, SessionTokenExpiry: time.Duration(time.Hour), }, + DashboardFinalRedirectURL: "", } _ = startCandid(c, &p) svc, err := jimm.NewService(context.Background(), p) From 0d3ec34f6db5c2f465401f42d1c45d5ff66c05fa Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:25:49 +0000 Subject: [PATCH 072/126] Css 6646/sessions (#1171) * feat(oauth login browser): implements /auth/login As discussed on call, we integration test only the handler and avoid mocks to see the behaviour is as expected. This is also true when we come to implements the callback logic and are required to start the flow from scratch. The current test in auth_handler_test will be updated to cover the entire flow when implementing /callback. As for the state todo, I need to see how to track the state between requests and the TODO will be completed in the /callback PR. 6646 * pr comments * pr comments * feat(browser flow for dashboard): implements browser flow (without sessions) This PR includes a small refactor to the admin device flow, such that it can share the same identity update logic within the browser flow. * feat(validation in auth handler): validates params given are correct and auth svc not nill * Fix tests * pr comments * additional failure tests * test fix * Update refresh token * pr comments * Handle sessions * Comment params * pr comments * bad comments --- cmd/jimmsrv/main.go | 18 +++++++ docker-compose.yaml | 2 + go.mod | 7 ++- go.sum | 9 +++- internal/auth/oauth2.go | 16 ++++-- internal/jimm/jimm.go | 4 ++ internal/jimmhttp/auth_handler.go | 67 +++++++++++++++++++++----- internal/jimmhttp/auth_handler_test.go | 48 ++++++++++++++---- service.go | 41 +++++++++++++++- 9 files changed, 183 insertions(+), 29 deletions(-) diff --git a/cmd/jimmsrv/main.go b/cmd/jimmsrv/main.go index d10ccce26..79c5b052f 100644 --- a/cmd/jimmsrv/main.go +++ b/cmd/jimmsrv/main.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "os" + "strconv" "strings" "syscall" "time" @@ -118,6 +119,21 @@ func start(ctx context.Context, s *service.Service) error { if _, ok := os.LookupEnv("INSECURE_SECRET_STORAGE"); ok { insecureSecretStorage = true } + + secureSessionCookies := false + if _, ok := os.LookupEnv("JIMM_SECURE_SESSION_COOKIES"); ok { + secureSessionCookies = true + } + + sessionCookieExpiry := os.Getenv("JIMM_SESSION_COOKIE_EXPIRY") + sessionCookieExpiryInt, err := strconv.Atoi(sessionCookieExpiry) + if err != nil { + return errors.E("unable to parse jimm session cookie expiry") + } + if sessionCookieExpiryInt < 0 { + return errors.E("jimm session cookie expiry cannot be less than 0") + } + jimmsvc, err := jimm.NewService(ctx, jimm.Params{ ControllerUUID: os.Getenv("JIMM_UUID"), DSN: os.Getenv("JIMM_DSN"), @@ -153,6 +169,8 @@ func start(ctx context.Context, s *service.Service) error { SessionTokenExpiry: sessionTokenExpiryDuration, }, DashboardFinalRedirectURL: os.Getenv("JIMM_DASHBOARD_FINAL_REDIRECT_URL"), + SecureSessionCookies: secureSessionCookies, + SessionCookieExpiry: sessionCookieExpiryInt, }) if err != nil { return err diff --git a/docker-compose.yaml b/docker-compose.yaml index a74026e50..5303fd933 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -80,6 +80,8 @@ services: JIMM_OAUTH_SCOPES: "openid profile email" # Space separated list of scopes JIMM_DASHBOARD_FINAL_REDIRECT_URL: "https://my-dashboard.com/final-callback" # Example URL JIMM_ACCESS_TOKEN_EXPIRY_DURATION: 1h + JIMM_SECURE_SESSION_COOKIES: false + JIMM_SESSION_COOKIE_EXPIRY: 86400 volumes: - ./:/jimm/ - ./local/vault/approle.json:/vault/approle.json:rw diff --git a/go.mod b/go.mod index ee3546836..de3617fde 100644 --- a/go.mod +++ b/go.mod @@ -83,6 +83,7 @@ require ( github.com/Rican7/retry v0.3.1 // indirect github.com/adrg/xdg v0.3.3 // indirect github.com/ajg/form v1.5.1 // indirect + github.com/antonlindstrom/pgstore v0.0.0-20220421113606-e3a6e3fed12a // indirect github.com/armon/go-metrics v0.4.0 // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/aws/aws-sdk-go-v2 v1.21.0 // indirect @@ -147,14 +148,15 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/certificate-transparency-go v1.1.2-0.20210511102531-373a877eec92 // indirect - github.com/google/gofuzz v1.1.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/google/renameio v1.0.1 // indirect github.com/google/s2a-go v0.1.4 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect github.com/googleapis/gax-go/v2 v2.11.0 // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/gorilla/schema v1.2.0 // indirect - github.com/gorilla/securecookie v1.1.1 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.2.1 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect @@ -249,6 +251,7 @@ require ( github.com/lestrrat/go-jsval v0.0.0-20161012045717-b1258a10419f // indirect github.com/lestrrat/go-pdebug v0.0.0-20160817063333-2e6eaaa5717f // indirect github.com/lestrrat/go-structinfo v0.0.0-20160308131105-f74c056fe41f // indirect + github.com/lib/pq v1.10.7 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/magiconair/properties v1.8.7 // indirect diff --git a/go.sum b/go.sum index 21298371b..dbc6fe20c 100644 --- a/go.sum +++ b/go.sum @@ -179,6 +179,8 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antonlindstrom/pgstore v0.0.0-20220421113606-e3a6e3fed12a h1:dIdcLbck6W67B5JFMewU5Dba1yKZA3MsT67i4No/zh0= +github.com/antonlindstrom/pgstore v0.0.0-20220421113606-e3a6e3fed12a/go.mod h1:Sdr/tmSOLEnncCuXS5TwZRxuk7deH1WXVY8cve3eVBM= github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= github.com/apache/beam v2.28.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -750,8 +752,9 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/licenseclassifier v0.0.0-20210325184830-bb04aff29e72/go.mod h1:qsqn2hxC+vURpyBRygGUuinTO42MFRLcsmQ/P8v94+M= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -813,8 +816,10 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= -github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index ac90fe244..9a655689a 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -219,7 +219,7 @@ func (as *AuthenticationService) Email(idToken *oidc.IDToken) (string, error) { // via an access token. The token only contains the user's email for authentication. func (as *AuthenticationService) MintSessionToken(email string, secretKey string) (string, error) { const op = errors.Op("auth.AuthenticationService.MintAccessToken") - + token, err := jwt.NewBuilder(). Subject(email). Expiration(time.Now().Add(as.sessionTokenExpiry)). @@ -244,6 +244,8 @@ func (as *AuthenticationService) VerifySessionToken(token string, secretKey stri // UpdateIdentity updates the database with the display name and access token set for the user. // And, if present, a refresh token. func (as *AuthenticationService) UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error { + const op = errors.Op("auth.UpdateIdentity") + db := as.db u := &dbmodel.Identity{ Name: email, @@ -254,16 +256,22 @@ func (as *AuthenticationService) UpdateIdentity(ctx context.Context, email strin // this should be changed and split apart so it is intentional what entities // we are creating or fetching. if err := db.GetIdentity(ctx, u); err != nil { - return err + return errors.E(op, err) } // Check if user has a display name, if not, set one if u.DisplayName == "" { - u.DisplayName = strings.Split(email, "@")[0] + splitEmail := strings.Split(email, "@") + if len(splitEmail) > 0 { + u.DisplayName = strings.Split(email, "@")[0] + } else { + return errors.E(op, "failed to split email") + } } + u.AccessToken = token.AccessToken u.RefreshToken = token.RefreshToken if err := db.UpdateIdentity(ctx, u); err != nil { - return err + return errors.E(op, err) } return nil diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index 9a2a5114d..1d07fa3ec 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/antonlindstrom/pgstore" "github.com/coreos/go-oidc/v3/oidc" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/juju/api/base" @@ -89,6 +90,9 @@ type JIMM struct { // OAuthAuthenticator is responsible for handling authentication // via OAuth2.0 AND JWT access tokens to JIMM. OAuthAuthenticator OAuthAuthenticator + + // CookieSessionStore is respnsible for handling cookie based sessions. + CookieSessionStore *pgstore.PGStore } // OAuthAuthenticationService returns the JIMM's authentication service. diff --git a/internal/jimmhttp/auth_handler.go b/internal/jimmhttp/auth_handler.go index bcc12af67..ac33cc379 100644 --- a/internal/jimmhttp/auth_handler.go +++ b/internal/jimmhttp/auth_handler.go @@ -4,6 +4,7 @@ import ( "context" "net/http" + "github.com/antonlindstrom/pgstore" "github.com/coreos/go-oidc/v3/oidc" "github.com/go-chi/chi/v5" "github.com/juju/zaputil/zapctx" @@ -17,8 +18,31 @@ import ( // Implements jimmhttp.JIMMHttpHandler. type OAuthHandler struct { Router *chi.Mux - Authenticator BrowserOAuthAuthenticator + authenticator BrowserOAuthAuthenticator + dashboardFinalRedirectURL string + sessionStore *pgstore.PGStore + secureCookies bool + cookieExpiry int +} + +// OAuthHandlerParams holds the parameters to configure the OAuthHandler. +type OAuthHandlerParams struct { + // Authenticator is the authenticator to handle browser authentication. + Authenticator BrowserOAuthAuthenticator + + // DashboardFinalRedirectURL is the final redirection URL to send users to + // upon completing the authorisation code flow. DashboardFinalRedirectURL string + + // SessionStore is the cookie session store. + SessionStore *pgstore.PGStore + + // SessionCookies determines if HTTPS must be enabled in order for JIMM + // to set cookies when creating browser based sessions. + SecureCookies bool + + // CookieExpiry is how long the cookie will be valid before expiring in seconds. + CookieExpiry int } // BrowserOAuthAuthenticator handles authorisation code authentication within JIMM @@ -32,17 +56,23 @@ type BrowserOAuthAuthenticator interface { } // NewOAuthHandler returns a new OAuth handler. -func NewOAuthHandler(authenticator BrowserOAuthAuthenticator, dashboardFinalRedirectURL string) (*OAuthHandler, error) { - if authenticator == nil { +func NewOAuthHandler(p OAuthHandlerParams) (*OAuthHandler, error) { + if p.Authenticator == nil { return nil, errors.E("nil authenticator") } - if dashboardFinalRedirectURL == "" { + if p.DashboardFinalRedirectURL == "" { return nil, errors.E("final redirect url not specified") } + if p.SessionStore == nil { + return nil, errors.E("nil session store") + } return &OAuthHandler{ Router: chi.NewRouter(), - Authenticator: authenticator, - DashboardFinalRedirectURL: dashboardFinalRedirectURL, + authenticator: p.Authenticator, + dashboardFinalRedirectURL: p.DashboardFinalRedirectURL, + sessionStore: p.SessionStore, + secureCookies: p.SecureCookies, + cookieExpiry: p.CookieExpiry, }, nil } @@ -60,7 +90,7 @@ func (oah *OAuthHandler) SetupMiddleware() { // Login handles /auth/login. func (oah *OAuthHandler) Login(w http.ResponseWriter, r *http.Request) { - redirectURL := oah.Authenticator.AuthCodeURL() + redirectURL := oah.authenticator.AuthCodeURL() http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect) } @@ -69,14 +99,13 @@ func (oah *OAuthHandler) Callback(w http.ResponseWriter, r *http.Request) { ctx := context.Background() code := r.URL.Query().Get("code") - - authSvc := oah.Authenticator - if code == "" { writeError(ctx, w, http.StatusBadRequest, nil, "no authorisation code present") return } + authSvc := oah.authenticator + token, err := authSvc.Exchange(ctx, code) if err != nil { writeError(ctx, w, http.StatusBadRequest, err, "failed to exchange authcode") @@ -100,7 +129,23 @@ func (oah *OAuthHandler) Callback(w http.ResponseWriter, r *http.Request) { return } - http.Redirect(w, r, oah.DashboardFinalRedirectURL, http.StatusPermanentRedirect) + // If the session is empty, it'll just be an empty session, we only check + // errors for bad decoding etc. + session, err := oah.sessionStore.Get(r, "jimm-browser-session") + if err != nil { + writeError(ctx, w, http.StatusBadRequest, err, "failed to get session") + } + + session.IsNew = true // Sets cookie to a fresh new cookie + session.Options.MaxAge = oah.cookieExpiry // Expiry in seconds + session.Options.Secure = oah.secureCookies // Ensures only sent with HTTPS + session.Options.HttpOnly = false // Allow Javascript to read it + + session.Values["jimm-session"] = email + if err = session.Save(r, w); err != nil { + writeError(ctx, w, http.StatusBadRequest, err, "failed to save session") + } + http.Redirect(w, r, oah.dashboardFinalRedirectURL, http.StatusPermanentRedirect) } // writeError writes an error and logs the message. It is expected that the status code diff --git a/internal/jimmhttp/auth_handler_test.go b/internal/jimmhttp/auth_handler_test.go index d274fd8ab..849623985 100644 --- a/internal/jimmhttp/auth_handler_test.go +++ b/internal/jimmhttp/auth_handler_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + "github.com/antonlindstrom/pgstore" "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" @@ -24,7 +25,23 @@ import ( "github.com/canonical/jimm/internal/jimmtest" ) -func setupTestServer(c *qt.C, dashboardURL string) *httptest.Server { +func setupDbAndSessionStore(c *qt.C) (*db.Database, *pgstore.PGStore) { + // Setup db ahead of time so we have access to session store + db := &db.Database{ + DB: jimmtest.PostgresDB(c, func() time.Time { return time.Now() }), + } + c.Assert(db.Migrate(context.Background(), false), qt.IsNil) + + sqlDb, err := db.DB.DB() + c.Assert(err, qt.IsNil) + + store, err := pgstore.NewPGStoreFromPool(sqlDb, []byte("secretsecretdigletts")) + c.Assert(err, qt.IsNil) + + return db, store +} + +func setupTestServer(c *qt.C, dashboardURL string, db *db.Database, sessionStore *pgstore.PGStore) *httptest.Server { // Create unstarted server to enable auth service s := httptest.NewUnstartedServer(nil) // Setup random port listener @@ -39,10 +56,6 @@ func setupTestServer(c *qt.C, dashboardURL string) *httptest.Server { // Remember redirect url to check it matches after test server starts redirectURL := "http://127.0.0.1:" + port + "/callback" - db := &db.Database{ - DB: jimmtest.PostgresDB(c, func() time.Time { return time.Now() }), - } - c.Assert(db.Migrate(context.Background(), false), qt.IsNil) authSvc, err := auth.NewAuthenticationService(context.Background(), auth.AuthenticationServiceParams{ IssuerURL: "http://localhost:8082/realms/jimm", ClientID: "jimm-device", @@ -55,7 +68,13 @@ func setupTestServer(c *qt.C, dashboardURL string) *httptest.Server { }) c.Assert(err, qt.IsNil) - h, err := jimmhttp.NewOAuthHandler(authSvc, dashboardURL) + h, err := jimmhttp.NewOAuthHandler(jimmhttp.OAuthHandlerParams{ + Authenticator: authSvc, + DashboardFinalRedirectURL: dashboardURL, + SessionStore: sessionStore, + SecureCookies: false, + CookieExpiry: 86400, + }) c.Assert(err, qt.IsNil) s.Config.Handler = h.Routes() @@ -76,6 +95,8 @@ func setupTestServer(c *qt.C, dashboardURL string) *httptest.Server { func TestBrowserAuth(t *testing.T) { c := qt.New(t) + db, sessionStore := setupDbAndSessionStore(c) + // Setup final test redirect url server, to emulate // the dashboard receiving the final piece of the flow dashboardResponse := "dashboard received final callback" @@ -83,12 +104,19 @@ func TestBrowserAuth(t *testing.T) { http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, dashboardResponse) + sessionCookie, _ := r.Cookie("jimm-browser-session") + c.Assert(sessionCookie.Name, qt.Equals, "jimm-browser-session") + c.Assert(sessionCookie.Value, qt.Not(qt.Equals), "") + // Check the session exist in db + session, err := sessionStore.Get(r, "jimm-browser-session") + c.Assert(err, qt.IsNil) + c.Assert(session.Values["jimm-session"], qt.Equals, "jimm-test@canonical.com") }, ), ) defer dashboard.Close() - s := setupTestServer(c, dashboard.URL) + s := setupTestServer(c, dashboard.URL, db, sessionStore) defer s.Close() jar, err := cookiejar.New(nil) @@ -132,7 +160,8 @@ func TestBrowserAuth(t *testing.T) { func TestCallbackFailsNoCodePresent(t *testing.T) { c := qt.New(t) - s := setupTestServer(c, "") + db, sessionStore := setupDbAndSessionStore(c) + s := setupTestServer(c, "", db, sessionStore) defer s.Close() // Test with no code present at all @@ -149,7 +178,8 @@ func TestCallbackFailsNoCodePresent(t *testing.T) { func TestCallbackFailsExchange(t *testing.T) { c := qt.New(t) - s := setupTestServer(c, "") + db, sessionStore := setupDbAndSessionStore(c) + s := setupTestServer(c, "", db, sessionStore) defer s.Close() // Test with no code present at all diff --git a/service.go b/service.go index f59fab015..6fdb84d7f 100644 --- a/service.go +++ b/service.go @@ -4,13 +4,16 @@ package jimm import ( "context" + "database/sql" "encoding/json" "net/http" + "net/url" "os" "strconv" "strings" "time" + "github.com/antonlindstrom/pgstore" "github.com/canonical/candid/candidclient" cofga "github.com/canonical/ofga" "github.com/go-chi/chi/v5" @@ -184,6 +187,13 @@ type Params struct { // DashboardFinalRedirectURL is the URL to FINALLY redirect to after completing // the /callback in an authorisation code OAuth2.0 flow to finish the flow. DashboardFinalRedirectURL string + + // SecureSessionCookies determines if HTTPS must be enabled in order for JIMM + // to set cookies when creating browser based sessions. + SecureSessionCookies bool + + // SessionCookieExpiry is how long the cookie will be valid before expiring in seconds. + SessionCookieExpiry int } // A Service is the implementation of a JIMM server. @@ -264,6 +274,20 @@ func NewService(ctx context.Context, p Params) (*Service, error) { if err := s.jimm.Database.Migrate(ctx, false); err != nil { return nil, errors.E(op, err) } + sqlDb, err := s.jimm.Database.DB.DB() + if err != nil { + return nil, errors.E(op, err) + } + + // Setup browser session store + sessionStore, err := setupSessionStore(sqlDb, "secret-key-todo") + if err != nil { + return nil, errors.E(op, err) + } + + // Cleanup expired session every 30 minutes + defer sessionStore.StopCleanup(sessionStore.Cleanup(time.Minute * 30)) + s.jimm.CookieSessionStore = sessionStore if p.AuditLogRetentionPeriodInDays != "" { period, err := strconv.Atoi(p.AuditLogRetentionPeriodInDays) @@ -340,6 +364,10 @@ func NewService(ctx context.Context, p Params) (*Service, error) { s.jimm.Dialer = jimm.CacheDialer(s.jimm.Dialer) } + if _, err := url.Parse(p.DashboardFinalRedirectURL); err != nil { + return nil, errors.E(op, err, "failed to parse final redirect url for the dashboard") + } + mountHandler := func(path string, h jimmhttp.JIMMHttpHandler) { s.mux.Mount(path, h.Routes()) } @@ -356,7 +384,13 @@ func NewService(ctx context.Context, p Params) (*Service, error) { "/.well-known", wellknownapi.NewWellKnownHandler(s.jimm.CredentialStore), ) - oauthHandler, err := jimmhttp.NewOAuthHandler(authSvc, p.DashboardFinalRedirectURL) + oauthHandler, err := jimmhttp.NewOAuthHandler(jimmhttp.OAuthHandlerParams{ + Authenticator: authSvc, + DashboardFinalRedirectURL: p.DashboardFinalRedirectURL, + SessionStore: sessionStore, + SecureCookies: p.SecureSessionCookies, + CookieExpiry: p.SessionCookieExpiry, + }) if err != nil { return nil, errors.E(op, err, "failed to setup authentication handler") } @@ -403,6 +437,11 @@ func (s *Service) setupDischarger(p Params, openFGAclient *openfga.OFGAClient) ( return &macaroonDischarger.kp, dischargeMux, nil } +func setupSessionStore(db *sql.DB, secretKey string) (*pgstore.PGStore, error) { + store, err := pgstore.NewPGStoreFromPool(db, []byte(secretKey)) + return store, err +} + func openDB(ctx context.Context, dsn string) (*gorm.DB, error) { zapctx.Info(ctx, "connecting database") From 4948ae1b286ad42e81e313ad2112a7bc3ea72f15 Mon Sep 17 00:00:00 2001 From: alesstimec Date: Fri, 8 Mar 2024 09:29:34 +0100 Subject: [PATCH 073/126] Adds LoginWithClientCredentials to Admin facade version 4. --- api/params/params.go | 7 ++ internal/auth/oauth2.go | 21 +++++- internal/auth/oauth2_test.go | 19 +++++ internal/jimm/jimm.go | 3 + internal/jujuapi/admin.go | 38 ++++++++++ internal/jujuapi/admin_test.go | 29 ++++++++ internal/jujuapi/controllerroot.go | 1 + local/keycloak/jimm-realm.json | 114 ++++++++++++++++++++++++++++- 8 files changed, 227 insertions(+), 5 deletions(-) diff --git a/api/params/params.go b/api/params/params.go index 8c8d8bf62..d63bbb132 100644 --- a/api/params/params.go +++ b/api/params/params.go @@ -430,6 +430,13 @@ type LoginWithSessionTokenRequest struct { // Service Account related request parameters +// LoginWithClientCredentialsRequest holds the client id and secret used +// to authenticate with JIMM. +type LoginWithClientCredentialsRequest struct { + ClientID string `json:"client-id"` + ClientSecret string `json:"client-secret"` +} + // AddServiceAccountRequest holds a request to add a service account. type AddServiceAccountRequest struct { // ClientID holds the client id of the service account. diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 9a655689a..e052e96c4 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -16,6 +16,7 @@ import ( "github.com/lestrrat-go/jwx/v2/jwt" "go.uber.org/zap" "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" @@ -219,7 +220,7 @@ func (as *AuthenticationService) Email(idToken *oidc.IDToken) (string, error) { // via an access token. The token only contains the user's email for authentication. func (as *AuthenticationService) MintSessionToken(email string, secretKey string) (string, error) { const op = errors.Op("auth.AuthenticationService.MintAccessToken") - + token, err := jwt.NewBuilder(). Subject(email). Expiration(time.Now().Add(as.sessionTokenExpiry)). @@ -310,3 +311,21 @@ func VerifySessionToken(token string, secretKey string) (jwt.Token, error) { return parsedToken, nil } + +// VerifyClientCredentials verifies the provided client ID and client secret. +func (as *AuthenticationService) VerifyClientCredentials(ctx context.Context, clientID string, clientSecret string) error { + cfg := clientcredentials.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + TokenURL: as.oauthConfig.Endpoint.TokenURL, + Scopes: as.oauthConfig.Scopes, + AuthStyle: oauth2.AuthStyle(as.oauthConfig.Endpoint.AuthStyle), + } + + _, err := cfg.Token(ctx) + if err != nil { + zapctx.Error(ctx, "client credential verification failed", zap.Error(err)) + return errors.E(errors.CodeUnauthorized, "invalid client credentials") + } + return nil +} diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 5014c7347..d07e9e6e6 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -226,3 +226,22 @@ func TestSessionTokenValidatesEmail(t *testing.T) { _, err = authSvc.VerifySessionToken(token, secretKey) c.Assert(err, qt.ErrorMatches, "failed to parse email") } + +func TestVerifyClientCredentials(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + + const ( + // these are valid client credentials hardcoded into the jimm realm + validClientID = "test-client-id" + validClientSecret = "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf" + ) + + authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) + + err := authSvc.VerifyClientCredentials(ctx, validClientID, validClientSecret) + c.Assert(err, qt.IsNil) + + err = authSvc.VerifyClientCredentials(ctx, "invalid-client-id", validClientSecret) + c.Assert(err, qt.ErrorMatches, "invalid client credentials") +} diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index 1d07fa3ec..a0aa4ce59 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -173,6 +173,9 @@ type OAuthAuthenticator interface { // UpdateIdentity updates the database with the display name and access token set for the user. // And, if present, a refresh token. UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error + + // VerifyClientCredentials verifies the provided client ID and client secret. + VerifyClientCredentials(ctx context.Context, clientID string, clientSecret string) error } type permission struct { diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index a0d6e010c..3a2933f44 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -200,6 +200,44 @@ func (r *controllerRoot) LoginWithSessionToken(ctx context.Context, req params.L }, nil } +// LoginWithClientCredentials handles logging into the JIMM with the client ID +// and secret created by the IdP. +func (r *controllerRoot) LoginWithClientCredentials(ctx context.Context, req params.LoginWithClientCredentialsRequest) (jujuparams.LoginResult, error) { + const op = errors.Op("jujuapi.LoginWithClientCredentials") + + authenticationSvc := r.jimm.OAuthAuthenticationService() + if authenticationSvc == nil { + return jujuparams.LoginResult{}, errors.E("authentication service not specified") + } + err := authenticationSvc.VerifyClientCredentials(ctx, req.ClientID, req.ClientSecret) + if err != nil { + return jujuparams.LoginResult{}, errors.E(err, errors.CodeUnauthorized) + } + + user, err := r.jimm.GetOpenFGAUserAndAuthorise(ctx, req.ClientID) + if err != nil { + return jujuparams.LoginResult{}, errors.E(op, err) + } + + r.mu.Lock() + r.user = user + r.mu.Unlock() + + // Get server version for LoginResult + srvVersion, err := r.jimm.EarliestControllerVersion(ctx) + if err != nil { + return jujuparams.LoginResult{}, errors.E(op, err) + } + + return jujuparams.LoginResult{ + PublicDNSName: r.params.PublicDNSName, + UserInfo: setupAuthUserInfo(ctx, r, user), + ControllerTag: setupControllerTag(r), + Facades: setupFacades(r), + ServerVersion: srvVersion.String(), + }, nil +} + // setupControllerTag returns the String() of a controller tag based on the // JIMM controller UUID. func setupControllerTag(root *controllerRoot) string { diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index 9869ca567..89810c6b0 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -22,6 +22,7 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/juju/juju/api" jujuparams "github.com/juju/juju/rpc/params" + "github.com/juju/names/v4" gc "gopkg.in/check.v1" "gopkg.in/macaroon.v2" ) @@ -216,3 +217,31 @@ func handleLoginForm(c *gc.C, loginForm string, client *http.Client, username, p re = regexp.MustCompile(`Device Login Successful`) c.Assert(re.MatchString(string(b)), gc.Equals, true) } + +func (s *adminSuite) TestLoginWithClientCredentials(c *gc.C) { + conn := s.open(c, &api.Info{ + SkipLogin: true, + }, "test") + defer conn.Close() + + const ( + // these are valid client credentials hardcoded into the jimm realm + validClientID = "test-client-id" + validClientSecret = "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf" + ) + + var loginResult jujuparams.LoginResult + err := conn.APICall("Admin", 4, "", "LoginWithClientCredentials", params.LoginWithClientCredentialsRequest{ + ClientID: validClientID, + ClientSecret: validClientSecret, + }, &loginResult) + c.Assert(err, gc.IsNil) + c.Assert(loginResult.ControllerTag, gc.Equals, names.NewControllerTag(s.Params.ControllerUUID).String()) + c.Assert(loginResult.UserInfo.Identity, gc.Equals, names.NewUserTag("test-client-id").String()) + + err = conn.APICall("Admin", 4, "", "LoginWithClientCredentials", params.LoginWithClientCredentialsRequest{ + ClientID: "invalid-client-id", + ClientSecret: "invalid-secret", + }, &loginResult) + c.Assert(err, gc.ErrorMatches, `invalid client credentials \(unauthorized access\)`) +} diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index ef7a01385..e87fb8aa9 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -151,6 +151,7 @@ func newControllerRoot(j JIMM, p Params) *controllerRoot { r.AddMethod("Admin", 4, "LoginDevice", rpc.Method(r.LoginDevice)) r.AddMethod("Admin", 4, "GetDeviceSessionToken", rpc.Method(r.GetDeviceSessionToken)) r.AddMethod("Admin", 4, "LoginWithSessionToken", rpc.Method(r.LoginWithSessionToken)) + r.AddMethod("Admin", 4, "LoginWithClientCredentials", rpc.Method(r.LoginWithClientCredentials)) r.AddMethod("Pinger", 1, "Ping", rpc.Method(r.Ping)) return r } diff --git a/local/keycloak/jimm-realm.json b/local/keycloak/jimm-realm.json index f7ef299f4..cf0a1488d 100644 --- a/local/keycloak/jimm-realm.json +++ b/local/keycloak/jimm-realm.json @@ -459,7 +459,7 @@ "id": "8281cec3-5b48-46eb-a41d-72c15ec3f9e0", "username": "jimm-test", "email": "jimm-test@canonical.com", - "emailVerified":true, + "emailVerified": true, "enabled": true, "credentials": [ { @@ -467,9 +467,14 @@ "value": "password" } ], - "realmRoles": ["user"], + "realmRoles": [ + "user" + ], "clientRoles": { - "account": ["view-profile", "manage-account"] + "account": [ + "view-profile", + "manage-account" + ] } } ], @@ -687,6 +692,107 @@ "microprofile-jwt" ] }, + { + "clientId": "test-client-id", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "oauth2.device.authorization.grant.enabled": "false", + "client.secret.creation.time": "1709808812", + "backchannel.logout.session.required": "true", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "client_id", + "introspection.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "client_id", + "jsonType.label": "String" + } + }, + { + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "introspection.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "introspection.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "access": { + "view": true, + "configure": true, + "manage": true + } + }, { "clientId": "jimm-device", "name": "jimm-testing", @@ -2278,4 +2384,4 @@ "clientPolicies": { "policies": [] } -} +} \ No newline at end of file From 0c679a6f6efbaa66463c064f8ba41dc5ce8614e8 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:03:59 +0200 Subject: [PATCH 074/126] CSS-7418 rip candid (#1167) * Update Juju dep * Removed Candid * Fix discharger * Add ability to add admin users * Update @external to @canonical.com * Fix failing tests * Fix jujuapi tests * Fix everyone access tests * Remove final mentions of JIMM - Excluding charm changes * Fix cmd tests * Fix jimmctl tests * Remove unneeded auth files - Also fixed the device flow tests * Fix service.go tests * Fix for running tests on GH and revert removal - Removed unnecessary line --- .github/workflows/ci.yaml | 8 +- Makefile | 16 +- README.md | 11 + charms/bundles/controller/README.md | 2 +- charms/how-to-deploy-jimm-k8s.md | 12 +- cmd/jaas/cmd/addserviceaccount_test.go | 9 +- cmd/jaas/cmd/export_test.go | 17 +- cmd/jaas/cmd/grant_test.go | 9 +- .../cmd/listserviceaccountcredentials_test.go | 7 +- cmd/jaas/cmd/updatecredentials.go | 2 +- cmd/jaas/cmd/updatecredentials_test.go | 17 +- cmd/jimmctl/cmd/addcloudtocontroller.go | 2 +- cmd/jimmctl/cmd/addcloudtocontroller_test.go | 9 +- cmd/jimmctl/cmd/addcontroller_test.go | 4 +- cmd/jimmctl/cmd/crossmodelquery_test.go | 8 +- cmd/jimmctl/cmd/export_test.go | 97 +- cmd/jimmctl/cmd/grantauditlogaccess.go | 2 +- cmd/jimmctl/cmd/grantauditlogaccess_test.go | 9 +- cmd/jimmctl/cmd/group_test.go | 19 +- cmd/jimmctl/cmd/importcloudcredentials.go | 2 +- .../cmd/importcloudcredentials_test.go | 15 +- cmd/jimmctl/cmd/importmodel.go | 4 +- cmd/jimmctl/cmd/importmodel_test.go | 36 +- cmd/jimmctl/cmd/listauditevents_test.go | 20 +- cmd/jimmctl/cmd/listcontrollers_test.go | 4 +- cmd/jimmctl/cmd/migratemodel.go | 2 +- cmd/jimmctl/cmd/migratemodel_test.go | 18 +- cmd/jimmctl/cmd/modelstatus.go | 2 +- cmd/jimmctl/cmd/modelstatus_test.go | 14 +- cmd/jimmctl/cmd/purge_logs_test.go | 17 +- cmd/jimmctl/cmd/relation.go | 2 +- cmd/jimmctl/cmd/relation_test.go | 51 +- cmd/jimmctl/cmd/removecloudfromcontroller.go | 2 +- .../cmd/removecloudfromcontroller_test.go | 7 +- cmd/jimmctl/cmd/removecontroller_test.go | 4 +- cmd/jimmctl/cmd/revokeauditlogaccess.go | 2 +- cmd/jimmctl/cmd/revokeauditlogaccess_test.go | 9 +- .../cmd/setcontrollerdeprecated_test.go | 4 +- cmd/jimmctl/cmd/updatemigratedmodel.go | 2 +- cmd/jimmctl/cmd/updatemigratedmodel_test.go | 22 +- cmd/jimmsrv/main.go | 3 - docker-compose.yaml | 24 - go.mod | 303 ++- go.sum | 1719 +++-------------- internal/auth/auth.go | 10 - internal/auth/client.go | 63 - internal/auth/jujuauth.go | 63 - internal/auth/jujuauth_test.go | 221 --- internal/auth/oauth2.go | 6 + internal/cmdtest/jimmsuite.go | 113 +- internal/db/applicationoffer_test.go | 4 +- internal/db/auditlog_test.go | 20 +- internal/db/cloudcredential_test.go | 34 +- internal/db/clouddefaults.go | 2 +- internal/db/clouddefaults_test.go | 4 +- internal/db/controller_test.go | 14 +- internal/db/db_test.go | 2 +- internal/db/identity_test.go | 6 +- internal/db/identitymodeldefaults_test.go | 8 +- internal/db/model_test.go | 42 +- internal/db/secrets.go | 2 +- internal/db/secrets_test.go | 2 +- internal/dbmodel/applicationoffer.go | 4 +- internal/dbmodel/applicationoffer_test.go | 2 +- internal/dbmodel/audit_test.go | 8 +- internal/dbmodel/cloud.go | 2 +- internal/dbmodel/cloud_test.go | 2 +- internal/dbmodel/cloudcredential.go | 2 +- internal/dbmodel/cloudcredential_test.go | 8 +- internal/dbmodel/controller.go | 2 +- internal/dbmodel/controller_test.go | 4 +- internal/dbmodel/group.go | 2 +- internal/dbmodel/identity.go | 4 +- internal/dbmodel/identity_test.go | 24 +- internal/dbmodel/model.go | 2 +- internal/dbmodel/model_test.go | 20 +- .../discharger/discharger.go | 43 +- internal/jimm/access.go | 9 +- internal/jimm/access_test.go | 30 +- internal/jimm/applicationoffer.go | 4 +- internal/jimm/applicationoffer_test.go | 172 +- internal/jimm/audit_log.go | 2 +- internal/jimm/cache.go | 2 +- internal/jimm/cache_test.go | 2 +- internal/jimm/cloud.go | 4 +- internal/jimm/cloud_test.go | 256 +-- internal/jimm/cloudcredential.go | 2 +- internal/jimm/cloudcredential_test.go | 140 +- internal/jimm/clouddefaults.go | 2 +- internal/jimm/clouddefaults_test.go | 22 +- internal/jimm/controller.go | 2 +- internal/jimm/controller_test.go | 198 +- internal/jimm/credentials/credentials.go | 2 +- internal/jimm/export_test.go | 2 +- internal/jimm/identitymodeldefaults_test.go | 10 +- internal/jimm/jimm.go | 15 +- internal/jimm/jimm_test.go | 96 +- internal/jimm/model.go | 5 +- internal/jimm/model_status_parser.go | 2 +- internal/jimm/model_status_parser_test.go | 20 +- internal/jimm/model_test.go | 814 ++++---- internal/jimm/modelsummary.go | 2 +- internal/jimm/modelsummary_test.go | 4 +- internal/jimm/service_account_test.go | 6 +- internal/jimm/user.go | 49 - internal/jimm/user_test.go | 92 +- internal/jimm/watcher.go | 2 +- internal/jimm/watcher_test.go | 42 +- internal/jimmjwx/jwt_test.go | 6 +- internal/jimmtest/api.go | 2 +- internal/jimmtest/auth.go | 36 + internal/jimmtest/env.go | 2 +- internal/jimmtest/jimm_mock.go | 2 +- internal/jimmtest/store.go | 2 +- internal/jimmtest/suite.go | 91 +- internal/jujuapi/access_control.go | 2 +- internal/jujuapi/access_control_test.go | 10 +- internal/jujuapi/admin.go | 56 +- internal/jujuapi/admin_test.go | 12 +- internal/jujuapi/api.go | 4 - internal/jujuapi/applicationoffers.go | 2 +- internal/jujuapi/applicationoffers_test.go | 104 +- internal/jujuapi/cloud.go | 2 +- internal/jujuapi/cloud_test.go | 80 +- internal/jujuapi/controller.go | 6 +- internal/jujuapi/controller_test.go | 18 +- internal/jujuapi/controllerroot.go | 3 +- internal/jujuapi/jimm.go | 2 +- internal/jujuapi/jimm_test.go | 50 +- internal/jujuapi/modelmanager.go | 2 +- internal/jujuapi/modelmanager_test.go | 202 +- internal/jujuapi/service_account.go | 2 +- internal/jujuapi/service_account_test.go | 4 +- internal/jujuapi/usermanager_test.go | 19 +- internal/jujuapi/websocket.go | 3 +- internal/jujuapi/websocket_test.go | 93 +- internal/jujuclient/applicationoffers.go | 2 +- internal/jujuclient/applicationoffers_test.go | 71 +- internal/jujuclient/client_test.go | 6 +- internal/jujuclient/cloud.go | 2 +- internal/jujuclient/cloud_test.go | 28 +- internal/jujuclient/dial.go | 6 +- internal/jujuclient/dial_test.go | 2 +- internal/jujuclient/modelmanager.go | 2 +- internal/jujuclient/modelmanager_test.go | 30 +- internal/jujuclient/ping_test.go | 2 +- internal/jujuclient/storage.go | 2 +- internal/jujuclient/storage_test.go | 14 +- internal/openfga/names/names.go | 4 +- internal/openfga/names/names_test.go | 2 +- internal/openfga/openfga.go | 4 +- internal/openfga/openfga_test.go | 10 +- internal/openfga/user.go | 2 +- internal/openfga/user_test.go | 2 +- internal/rpc/client_test.go | 2 +- internal/rpc/dial.go | 2 +- internal/rpc/proxy.go | 2 +- internal/vault/vault.go | 2 +- internal/vault/vault_test.go | 6 +- local/candid/config.yaml | 37 - local/candid/entry.sh | 8 - local/seed_db/main.go | 2 +- local/traefik/certs/certs.sh | 7 + pkg/names/applicationoffer.go | 2 +- pkg/names/names.go | 2 +- service.go | 157 +- service_test.go | 81 +- 167 files changed, 2373 insertions(+), 4248 deletions(-) delete mode 100644 internal/auth/auth.go delete mode 100644 internal/auth/client.go delete mode 100644 internal/auth/jujuauth_test.go rename discharger.go => internal/discharger/discharger.go (72%) delete mode 100644 local/candid/config.yaml delete mode 100755 local/candid/entry.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0f32ab693..83b1e483d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -42,6 +42,8 @@ jobs: run: | touch ./local/vault/approle.json touch ./local/vault/roleid.txt + - name: Create test certs + run: make certs - name: Start test environment run: docker compose up -d --wait - name: Build and Test @@ -65,12 +67,6 @@ jobs: - uses: actions/setup-go@v4 with: go-version-file: 'go.mod' - - name: Pull candid repo for test environment - run: | - git clone https://github.com/canonical/candid.git ./tmp/candid - cd ./tmp/candid - make image - docker image ls candid - name: Add volume files run: | touch ./local/vault/approle.json diff --git a/Makefile b/Makefile index fe081ab65..88dd5751f 100644 --- a/Makefile +++ b/Makefile @@ -29,15 +29,17 @@ clean: -$(RM) -r jimm-release/ -$(RM) jimm-*.tar.xz -test-env: sysdeps +certs: + @cd local/traefik/certs; ./certs.sh; cd - + +test-env: sysdeps certs @touch ./local/vault/approle.json && touch ./local/vault/roleid.txt - @docker compose up --force-recreate + @docker compose up --force-recreate -d --wait test-env-cleanup: @docker compose down -v --remove-orphans -dev-env-setup: sysdeps pull/candid - @cd local/traefik/certs; ./certs.sh; cd - +dev-env-setup: sysdeps certs @touch ./local/vault/approle.json && touch ./local/vault/roleid.txt @make version/commit.txt && make version/version.txt @go mod vendor @@ -97,11 +99,6 @@ push-microk8s: jimm-image docker tag jimm:latest localhost:32000/jimm:latest docker push localhost:32000/jimm:latest -pull/candid: - -git clone https://github.com/canonical/candid.git ./tmp/candid - (cd ./tmp/candid && make image) - docker image ls candid - get-local-auth: @go run ./local/authy @@ -138,7 +135,6 @@ help: @echo 'make sysdeps - Install the development environment system packages.' @echo 'make format - Format the source files.' @echo 'make simplify - Format and simplify the source files.' - @echo 'make pull/candid - Pull candid for local development environment.' @echo 'make get-local-auth - Get local auth to the API WSS endpoint locally.' .PHONY: build check install release clean format server simplify sysdeps help FORCE diff --git a/README.md b/README.md index d09a2037c..789da1a7f 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,12 @@ See [here](./local/README.md) on how to get started. ## Testing +## TLDR +Run: +``` +$ make test-env +$ go test ./... +``` ### Pre-requisite To check if your system has all the prequisites installed simply run `make sysdeps`. This will check for all test prequisites and inform you how to install them if not installed. @@ -56,6 +62,11 @@ This can be installed via: `sudo snap install juju-db`. The latest JIMM has an upgraded dependency on Juju which requires in turn requires juju-db from channel `4.4/stable`, this can be installed with `sudo snap install juju-db --channel=4.4/stable` +Tests inside of `cmd/` create a JIMM server and test the jimmctl and jaas CLI packages. The Juju CLI requires that it connects to +an HTTPS server, but these tests also start a Juju controller which expects to be able to fetch a JWKS and macaroon publickey +from JIMM (which is running as an HTTPS server). This would normally result in a TLS certificate error, however JIMM will +attempt to use a custom self-signed cert from the certificate generated in `local/traefik/certs`. The make command `make certs` will generate these certs and place the CA in your system's cert pool which will be picked up by the Go HTTP client. + The rest of the suite relies on PostgreSQL, OpenFGA and Hashicorp Vault which are dockerised and as such you may simple run `make test-env` to be integration test ready. The above command won't start a dockerised instance of JIMM as tests are normally run locally. Instead, to start a diff --git a/charms/bundles/controller/README.md b/charms/bundles/controller/README.md index c12693c97..6e07da11c 100644 --- a/charms/bundles/controller/README.md +++ b/charms/bundles/controller/README.md @@ -21,7 +21,7 @@ This bundle needs to be deployed on top of an already existing controller model. To bootstrap an appropriate model run commands like the following: - juju bootstrap --bootstrap-constraints="cores=8 mem=8G root-disk=50G" --config identity-url= --config allow-model-access=true --config public-dns-address=:443 / + juju bootstrap --bootstrap-constraints="cores=8 mem=8G root-disk=50G" --config allow-model-access=true --config public-dns-address=:443 / juju enable-ha -n 3 juju switch controller diff --git a/charms/how-to-deploy-jimm-k8s.md b/charms/how-to-deploy-jimm-k8s.md index 93d3a158c..3987340d3 100644 --- a/charms/how-to-deploy-jimm-k8s.md +++ b/charms/how-to-deploy-jimm-k8s.md @@ -26,17 +26,9 @@ juju switch jimm make push-microk8s //Switch to jimm-k8s charm directory charmcraft pack -juju deploy ./juju-jimm-k8s_ubuntu-20.04-amd64.charm --resource jimm-image="localhost:32000/jimm:latest" --config uuid=ff77dbd0-ab87-444e-b9c7-768c675bf59d --config dns-name=juju-jimm-k8s-0.juju-jimm-k8s-endpoints.jimm.svc.cluster.local --config candid-url="https://api.staging.jujucharms.com/identity" --config vault-access-address="" -// The following commands can be skipped but will prevent -// JIMM from communicating with Candid. -juju config juju-jimm-k8s private-key= -juju config juju-jimm-k8s public-key= -juju config juju-jimm-k8s candid-public-key= -juju config juju-jimm-k8s candid-agent-username= -juju config juju-jimm-k8s candid-agent-public-key= -juju config juju-jimm-k8s candid-agent-private-key= +juju deploy ./juju-jimm-k8s_ubuntu-20.04-amd64.charm --resource jimm-image="localhost:32000/jimm:latest" --config uuid=ff77dbd0-ab87-444e-b9c7-768c675bf59d --config dns-name=juju-jimm-k8s-0.juju-jimm-k8s-endpoints.jimm.svc.cluster.local --config vault-access-address="" ``` -Deploy OPNEFGA, make relations and run setup actions +Deploy OPENFGA, make relations and run setup actions ``` juju deploy openfga-k8s --series=jammy --channel=latest/edge --revision=5 juju relate juju-jimm-k8s openfga-k8s diff --git a/cmd/jaas/cmd/addserviceaccount_test.go b/cmd/jaas/cmd/addserviceaccount_test.go index 88ea8fbd2..32d9aa448 100644 --- a/cmd/jaas/cmd/addserviceaccount_test.go +++ b/cmd/jaas/cmd/addserviceaccount_test.go @@ -6,11 +6,12 @@ import ( "context" "github.com/juju/cmd/v3/cmdtesting" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jaas/cmd" "github.com/canonical/jimm/internal/cmdtest" + "github.com/canonical/jimm/internal/jimmtest" "github.com/canonical/jimm/internal/openfga" ofganames "github.com/canonical/jimm/internal/openfga/names" jimmnames "github.com/canonical/jimm/pkg/names" @@ -25,11 +26,11 @@ var _ = gc.Suite(&addServiceAccountSuite{}) func (s *addServiceAccountSuite) TestAddServiceAccount(c *gc.C) { clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") _, err := cmdtesting.RunCommand(c, cmd.NewAddServiceAccountCommandForTesting(s.ClientStore(), bClient), clientID) c.Assert(err, gc.IsNil) tuple := openfga.Tuple{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), } @@ -41,7 +42,7 @@ func (s *addServiceAccountSuite) TestAddServiceAccount(c *gc.C) { _, err = cmdtesting.RunCommand(c, cmd.NewAddServiceAccountCommandForTesting(s.ClientStore(), bClient), clientID) c.Assert(err, gc.IsNil) // Check that re-running the command for a different user returns an error. - bClientBob := s.UserBakeryClient("bob") + bClientBob := jimmtest.NewUserSessionLogin("bob") _, err = cmdtesting.RunCommand(c, cmd.NewAddServiceAccountCommandForTesting(s.ClientStore(), bClientBob), clientID) c.Assert(err, gc.ErrorMatches, "service account already owned") } diff --git a/cmd/jaas/cmd/export_test.go b/cmd/jaas/cmd/export_test.go index 7e8121749..6eed0feab 100644 --- a/cmd/jaas/cmd/export_test.go +++ b/cmd/jaas/cmd/export_test.go @@ -3,55 +3,54 @@ package cmd import ( - "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/cmd/v3" jujuapi "github.com/juju/juju/api" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" ) -func NewAddServiceAccountCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewAddServiceAccountCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &addServiceAccountCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewListServiceAccountCredentialsCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewListServiceAccountCredentialsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &listServiceAccountCredentialsCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewUpdateCredentialsCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewUpdateCredentialsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &updateCredentialsCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewGrantCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewGrantCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &grantCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } diff --git a/cmd/jaas/cmd/grant_test.go b/cmd/jaas/cmd/grant_test.go index d50e4c703..416789aa7 100644 --- a/cmd/jaas/cmd/grant_test.go +++ b/cmd/jaas/cmd/grant_test.go @@ -6,12 +6,13 @@ import ( "context" "github.com/juju/cmd/v3/cmdtesting" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jaas/cmd" "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/jimmtest" "github.com/canonical/jimm/internal/openfga" ofganames "github.com/canonical/jimm/internal/openfga/names" jimmnames "github.com/canonical/jimm/pkg/names" @@ -29,7 +30,7 @@ func (s *grantSuite) TestGrant(c *gc.C) { clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") sa := dbmodel.Identity{ Name: clientID, @@ -39,7 +40,7 @@ func (s *grantSuite) TestGrant(c *gc.C) { // Make alice admin of the service account tuple := openfga.Tuple{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), } @@ -85,7 +86,7 @@ func (s *grantSuite) TestMissingArgs(c *gc.C) { expectedError: "user/group not specified", }} - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") clientStore := s.ClientStore() for _, t := range tests { _, err := cmdtesting.RunCommand(c, cmd.NewGrantCommandForTesting(clientStore, bClient), t.args...) diff --git a/cmd/jaas/cmd/listserviceaccountcredentials_test.go b/cmd/jaas/cmd/listserviceaccountcredentials_test.go index cf8e3bd59..7769297bc 100644 --- a/cmd/jaas/cmd/listserviceaccountcredentials_test.go +++ b/cmd/jaas/cmd/listserviceaccountcredentials_test.go @@ -9,13 +9,14 @@ import ( jujucmd "github.com/juju/cmd/v3" "github.com/juju/cmd/v3/cmdtesting" "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jaas/cmd" "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/jimm" + "github.com/canonical/jimm/internal/jimmtest" "github.com/canonical/jimm/internal/openfga" ) @@ -36,7 +37,7 @@ func (s *listServiceAccountCredentialsSuite) TestListServiceAccountCredentials(c clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" // alice is superuser ctx := context.Background() - user := dbmodel.Identity{Name: "alice@external"} + user := dbmodel.Identity{Name: "alice@canonical.com"} u := openfga.NewUser(&user, s.OFGAClient) err = s.JIMM.AddServiceAccount(ctx, u, clientID) c.Assert(err, gc.IsNil) @@ -98,7 +99,7 @@ aws foo } for _, test := range testCases { c.Log(test.about) - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") var result *jujucmd.Context if test.showSecrets { result, err = cmdtesting.RunCommand(c, cmd.NewListServiceAccountCredentialsCommandForTesting(s.ClientStore(), bClient), clientID, "--format", test.format, "--show-secrets") diff --git a/cmd/jaas/cmd/updatecredentials.go b/cmd/jaas/cmd/updatecredentials.go index 7fdf98357..0d100ddfa 100644 --- a/cmd/jaas/cmd/updatecredentials.go +++ b/cmd/jaas/cmd/updatecredentials.go @@ -11,7 +11,7 @@ import ( jujucmd "github.com/juju/juju/cmd" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" - "github.com/juju/names/v4" + "github.com/juju/names/v5" jujuparams "github.com/juju/juju/rpc/params" diff --git a/cmd/jaas/cmd/updatecredentials_test.go b/cmd/jaas/cmd/updatecredentials_test.go index 7c158dc07..e803de39e 100644 --- a/cmd/jaas/cmd/updatecredentials_test.go +++ b/cmd/jaas/cmd/updatecredentials_test.go @@ -6,12 +6,13 @@ import ( "context" "github.com/juju/cmd/v3/cmdtesting" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jaas/cmd" "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/jimmtest" "github.com/canonical/jimm/internal/openfga" ofganames "github.com/canonical/jimm/internal/openfga/names" jimmnames "github.com/canonical/jimm/pkg/names" @@ -30,7 +31,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithNewCredentials(c *gc.C clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") sa := dbmodel.Identity{ Name: clientID, @@ -40,7 +41,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithNewCredentials(c *gc.C // Make alice admin of the service account tuple := openfga.Tuple{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), } @@ -91,7 +92,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") sa := dbmodel.Identity{ Name: clientID, @@ -101,7 +102,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c // Make alice admin of the service account tuple := openfga.Tuple{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), } @@ -156,7 +157,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c } func (s *updateCredentialsSuite) TestCloudNotInLocalStore(c *gc.C) { - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(s.ClientStore(), bClient), "00000000-0000-0000-0000-000000000000", "non-existing-cloud", @@ -166,7 +167,7 @@ func (s *updateCredentialsSuite) TestCloudNotInLocalStore(c *gc.C) { } func (s *updateCredentialsSuite) TestCredentialNotInLocalStore(c *gc.C) { - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") clientStore := s.ClientStore() err := clientStore.UpdateCredential("some-cloud", jujucloud.CloudCredential{ @@ -207,7 +208,7 @@ func (s *updateCredentialsSuite) TestMissingArgs(c *gc.C) { expectedError: "too many args", }} - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") clientStore := s.ClientStore() for _, t := range tests { _, err := cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(clientStore, bClient), t.args...) diff --git a/cmd/jimmctl/cmd/addcloudtocontroller.go b/cmd/jimmctl/cmd/addcloudtocontroller.go index 0d452c157..2bef3f72c 100644 --- a/cmd/jimmctl/cmd/addcloudtocontroller.go +++ b/cmd/jimmctl/cmd/addcloudtocontroller.go @@ -16,7 +16,7 @@ import ( "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/api" apiparams "github.com/canonical/jimm/api/params" diff --git a/cmd/jimmctl/cmd/addcloudtocontroller_test.go b/cmd/jimmctl/cmd/addcloudtocontroller_test.go index 98e59d71f..3c4052ed9 100644 --- a/cmd/jimmctl/cmd/addcloudtocontroller_test.go +++ b/cmd/jimmctl/cmd/addcloudtocontroller_test.go @@ -11,13 +11,14 @@ import ( "github.com/juju/cmd/v3/cmdtesting" "github.com/juju/juju/cloud" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" + "github.com/canonical/jimm/internal/jimmtest" "github.com/canonical/jimm/internal/openfga" ofganames "github.com/canonical/jimm/internal/openfga/names" ) @@ -34,7 +35,7 @@ func (s *addCloudToControllerSuite) SetUpTest(c *gc.C) { // We add user bob, who is a JIMM administrator. err := s.JIMM.Database.UpdateIdentity(context.Background(), &dbmodel.Identity{ DisplayName: "Bob", - Name: "bob@external", + Name: "bob@canonical.com", }) c.Assert(err, gc.IsNil) @@ -53,7 +54,7 @@ func (s *addCloudToControllerSuite) SetUpTest(c *gc.C) { // test-cloud. bob := openfga.NewUser( &dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", }, s.JIMM.OpenFGAClient, ) @@ -167,7 +168,7 @@ clouds: c.Log(test.about) tmpfile, cleanupFunc := writeTempFile(c, test.cloudInfo) - bClient := s.UserBakeryClient("bob@external") + bClient := jimmtest.NewUserSessionLogin("bob@canonical.com") // Running the command succeeds newCmd := cmd.NewAddCloudToControllerCommandForTesting(s.ClientStore(), bClient, test.cloudByNameFunc) var err error diff --git a/cmd/jimmctl/cmd/addcontroller_test.go b/cmd/jimmctl/cmd/addcontroller_test.go index b142d5668..aee99a0e1 100644 --- a/cmd/jimmctl/cmd/addcontroller_test.go +++ b/cmd/jimmctl/cmd/addcontroller_test.go @@ -38,7 +38,7 @@ func (s *addControllerSuite) TestAddControllerSuperuser(c *gc.C) { defer os.RemoveAll(tmpdir) // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") ctx, err := cmdtesting.RunCommand(c, cmd.NewAddControllerCommandForTesting(s.ClientStore(), bClient), tmpfile) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(ctx), gc.Matches, `name: controller-1 @@ -101,7 +101,7 @@ func (s *addControllerSuite) TestAddController(c *gc.C) { defer os.RemoveAll(tmpdir) // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewAddControllerCommandForTesting(s.ClientStore(), bClient), tmpfile) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/crossmodelquery_test.go b/cmd/jimmctl/cmd/crossmodelquery_test.go index 2e27f5d73..50cfec5c8 100644 --- a/cmd/jimmctl/cmd/crossmodelquery_test.go +++ b/cmd/jimmctl/cmd/crossmodelquery_test.go @@ -11,7 +11,7 @@ import ( "github.com/juju/cmd/v3/cmdtesting" jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/juju/testing/factory" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" ) @@ -24,12 +24,12 @@ var _ = gc.Suite(&crossModelQuerySuite{}) func (s *crossModelQuerySuite) TestCrossModelQueryCommand(c *gc.C) { // Test setup. store := s.ClientStore() - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") s.AddController(c, "controller-2", s.APIInfo(c)) - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/alice@external/cred") + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/alice@canonical.com/cred") s.UpdateCloudCredential(c, cct, jujuparams.CloudCredential{AuthType: "empty"}) - mt := s.AddModel(c, names.NewUserTag("alice@external"), "stg-o11y", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) + mt := s.AddModel(c, names.NewUserTag("alice@canonical.com"), "stg-o11y", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) state, _ := s.StatePool.Get(mt.Id()) f := factory.NewFactory(state.State, s.StatePool) app := f.MakeApplication(c, &factory.ApplicationParams{ diff --git a/cmd/jimmctl/cmd/export_test.go b/cmd/jimmctl/cmd/export_test.go index 6005c34b0..7b232d042 100644 --- a/cmd/jimmctl/cmd/export_test.go +++ b/cmd/jimmctl/cmd/export_test.go @@ -3,7 +3,6 @@ package cmd import ( - "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/cmd/v3" jujuapi "github.com/juju/juju/api" "github.com/juju/juju/cloud" @@ -19,73 +18,73 @@ var ( type AccessResult = accessResult -func NewListControllersCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewListControllersCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &listControllersCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewModelStatusCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewModelStatusCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &modelStatusCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewGrantAuditLogAccessCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewGrantAuditLogAccessCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &grantAuditLogAccessCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewRevokeAuditLogAccessCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewRevokeAuditLogAccessCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &revokeAuditLogAccessCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewListAuditEventsCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewListAuditEventsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &listAuditEventsCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewAddCloudToControllerCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client, cloudByNameFunc func(string) (*cloud.Cloud, error)) cmd.Command { +func NewAddCloudToControllerCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider, cloudByNameFunc func(string) (*cloud.Cloud, error)) cmd.Command { cmd := &addCloudToControllerCommand{ store: store, cloudByNameFunc: cloudByNameFunc, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } @@ -94,12 +93,12 @@ func NewAddCloudToControllerCommandForTesting(store jujuclient.ClientStore, bCli type RemoveCloudFromControllerAPI = removeCloudFromControllerAPI -func NewRemoveCloudFromControllerCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client, removeCloudFromControllerAPIFunc func() (RemoveCloudFromControllerAPI, error)) cmd.Command { +func NewRemoveCloudFromControllerCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider, removeCloudFromControllerAPIFunc func() (RemoveCloudFromControllerAPI, error)) cmd.Command { cmd := &removeCloudFromControllerCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, removeCloudFromControllerAPIFunc: removeCloudFromControllerAPIFunc, } @@ -110,24 +109,24 @@ func NewRemoveCloudFromControllerCommandForTesting(store jujuclient.ClientStore, return modelcmd.WrapBase(cmd) } -func NewAddControllerCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewAddControllerCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &addControllerCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewRemoveControllerCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewRemoveControllerCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &removeControllerCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } @@ -142,180 +141,180 @@ func NewControllerInfoCommandForTesting(store jujuclient.ClientStore) cmd.Comman return modelcmd.WrapBase(cmd) } -func NewSetControllerDeprecatedCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewSetControllerDeprecatedCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &setControllerDeprecatedCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewImportModelCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewImportModelCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &importModelCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewUpdateMigratedModelCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewUpdateMigratedModelCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &updateMigratedModelCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewImportCloudCredentialsCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewImportCloudCredentialsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &importCloudCredentialsCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewAddGroupCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewAddGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &addGroupCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewRenameGroupCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewRenameGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &renameGroupCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewRemoveGroupCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewRemoveGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &removeGroupCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewListGroupsCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewListGroupsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &listGroupsCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewAddRelationCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewAddRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &addRelationCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewRemoveRelationCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewRemoveRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &removeRelationCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewListRelationsCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewListRelationsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &listRelationsCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewCheckRelationCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewCheckRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &checkRelationCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewCrossModelQueryCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewCrossModelQueryCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &crossModelQueryCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewPurgeLogsCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewPurgeLogsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &purgeLogsCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } return modelcmd.WrapBase(cmd) } -func NewMigrateModelCommandForTesting(store jujuclient.ClientStore, bClient *httpbakery.Client) cmd.Command { +func NewMigrateModelCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &migrateModelCommand{ store: store, dialOpts: &jujuapi.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }, } diff --git a/cmd/jimmctl/cmd/grantauditlogaccess.go b/cmd/jimmctl/cmd/grantauditlogaccess.go index e8e6f4988..0c3843a6c 100644 --- a/cmd/jimmctl/cmd/grantauditlogaccess.go +++ b/cmd/jimmctl/cmd/grantauditlogaccess.go @@ -9,7 +9,7 @@ import ( jujucmd "github.com/juju/juju/cmd" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/api" apiparams "github.com/canonical/jimm/api/params" diff --git a/cmd/jimmctl/cmd/grantauditlogaccess_test.go b/cmd/jimmctl/cmd/grantauditlogaccess_test.go index 6036df171..bdf32dd0d 100644 --- a/cmd/jimmctl/cmd/grantauditlogaccess_test.go +++ b/cmd/jimmctl/cmd/grantauditlogaccess_test.go @@ -8,6 +8,7 @@ import ( "github.com/canonical/jimm/cmd/jimmctl/cmd" "github.com/canonical/jimm/internal/cmdtest" + "github.com/canonical/jimm/internal/jimmtest" ) type grantAuditLogAccessSuite struct { @@ -19,14 +20,14 @@ type grantAuditLogAccessSuite struct { func (s *grantAuditLogAccessSuite) TestGrantAuditLogAccessSuperuser(c *gc.C) { // alice is superuser - bClient := s.UserBakeryClient("alice") - _, err := cmdtesting.RunCommand(c, cmd.NewGrantAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@external") + bClient := jimmtest.NewUserSessionLogin("alice") + _, err := cmdtesting.RunCommand(c, cmd.NewGrantAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@canonical.com") c.Assert(err, gc.IsNil) } func (s *grantAuditLogAccessSuite) TestGrantAuditLogAccess(c *gc.C) { // bob is not superuser - bClient := s.UserBakeryClient("bob") - _, err := cmdtesting.RunCommand(c, cmd.NewGrantAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@external") + bClient := jimmtest.NewUserSessionLogin("bob") + _, err := cmdtesting.RunCommand(c, cmd.NewGrantAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@canonical.com") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/group_test.go b/cmd/jimmctl/cmd/group_test.go index ce3b02427..09f1cf306 100644 --- a/cmd/jimmctl/cmd/group_test.go +++ b/cmd/jimmctl/cmd/group_test.go @@ -13,6 +13,7 @@ import ( "github.com/canonical/jimm/cmd/jimmctl/cmd" "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/jimmtest" ) type groupSuite struct { @@ -23,7 +24,7 @@ var _ = gc.Suite(&groupSuite{}) func (s *groupSuite) TestAddGroupSuperuser(c *gc.C) { // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") _, err := cmdtesting.RunCommand(c, cmd.NewAddGroupCommandForTesting(s.ClientStore(), bClient), "test-group") c.Assert(err, gc.IsNil) @@ -36,14 +37,14 @@ func (s *groupSuite) TestAddGroupSuperuser(c *gc.C) { func (s *groupSuite) TestAddGroup(c *gc.C) { // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewAddGroupCommandForTesting(s.ClientStore(), bClient), "test-group") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *groupSuite) TestRenameGroupSuperuser(c *gc.C) { // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") err := s.JimmCmdSuite.JIMM.Database.AddGroup(context.TODO(), "test-group") c.Assert(err, gc.IsNil) @@ -60,14 +61,14 @@ func (s *groupSuite) TestRenameGroupSuperuser(c *gc.C) { func (s *groupSuite) TestRenameGroup(c *gc.C) { // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewRenameGroupCommandForTesting(s.ClientStore(), bClient), "test-group", "renamed-group") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *groupSuite) TestRemoveGroupSuperuser(c *gc.C) { // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") err := s.JimmCmdSuite.JIMM.Database.AddGroup(context.TODO(), "test-group") c.Assert(err, gc.IsNil) @@ -82,7 +83,7 @@ func (s *groupSuite) TestRemoveGroupSuperuser(c *gc.C) { func (s *groupSuite) TestRemoveGroupWithoutFlag(c *gc.C) { // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") _, err := cmdtesting.RunCommand(c, cmd.NewRemoveGroupCommandForTesting(s.ClientStore(), bClient), "test-group") c.Assert(err.Error(), gc.Matches, "Failed to read from input.") @@ -90,14 +91,14 @@ func (s *groupSuite) TestRemoveGroupWithoutFlag(c *gc.C) { func (s *groupSuite) TestRemoveGroup(c *gc.C) { // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewRemoveGroupCommandForTesting(s.ClientStore(), bClient), "test-group", "-y") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *groupSuite) TestListGroupsSuperuser(c *gc.C) { // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") for i := 0; i < 3; i++ { err := s.JimmCmdSuite.JIMM.Database.AddGroup(context.TODO(), fmt.Sprint("test-group", i)) @@ -114,7 +115,7 @@ func (s *groupSuite) TestListGroupsSuperuser(c *gc.C) { func (s *groupSuite) TestListGroups(c *gc.C) { // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewListGroupsCommandForTesting(s.ClientStore(), bClient), "test-group") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/importcloudcredentials.go b/cmd/jimmctl/cmd/importcloudcredentials.go index fdb2fbc4e..3400a5cfb 100644 --- a/cmd/jimmctl/cmd/importcloudcredentials.go +++ b/cmd/jimmctl/cmd/importcloudcredentials.go @@ -12,7 +12,7 @@ import ( jujucmd "github.com/juju/juju/cmd" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/errors" ) diff --git a/cmd/jimmctl/cmd/importcloudcredentials_test.go b/cmd/jimmctl/cmd/importcloudcredentials_test.go index 2dea10c8b..b7c9cd6c8 100644 --- a/cmd/jimmctl/cmd/importcloudcredentials_test.go +++ b/cmd/jimmctl/cmd/importcloudcredentials_test.go @@ -13,6 +13,7 @@ import ( "github.com/canonical/jimm/cmd/jimmctl/cmd" "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/jimmtest" ) type importCloudCredentialsSuite struct { @@ -22,7 +23,7 @@ type importCloudCredentialsSuite struct { var _ = gc.Suite(&importCloudCredentialsSuite{}) const creds = `{ - "_id": "aws/alice/test1", + "_id": "aws/alice@canonical.com/test1", "type": "access-key", "attributes": { "access-key": "key-id", @@ -30,7 +31,7 @@ const creds = `{ } } { - "_id": "aws/bob@external/test1", + "_id": "aws/bob@canonical.com/test1", "type": "access-key", "attributes": { "access-key": "key-id2", @@ -38,7 +39,7 @@ const creds = `{ } } { - "_id": "gce/charlie/test1", + "_id": "gce/charlie@canonical.com/test1", "type": "empty", "attributes": {} }` @@ -63,13 +64,13 @@ func (s *importCloudCredentialsSuite) TestImportCloudCredentials(c *gc.C) { c.Assert(err, gc.IsNil) // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") _, err = cmdtesting.RunCommand(c, cmd.NewImportCloudCredentialsCommandForTesting(s.ClientStore(), bClient), tmpfile) c.Assert(err, gc.IsNil) cred1 := dbmodel.CloudCredential{ CloudName: "aws", - OwnerIdentityName: "alice@external", + OwnerIdentityName: "alice@canonical.com", Name: "test1", } err = s.JIMM.Database.GetCloudCredential(context.Background(), &cred1) @@ -78,7 +79,7 @@ func (s *importCloudCredentialsSuite) TestImportCloudCredentials(c *gc.C) { cred2 := dbmodel.CloudCredential{ CloudName: "aws", - OwnerIdentityName: "bob@external", + OwnerIdentityName: "bob@canonical.com", Name: "test1", } err = s.JIMM.Database.GetCloudCredential(context.Background(), &cred2) @@ -87,7 +88,7 @@ func (s *importCloudCredentialsSuite) TestImportCloudCredentials(c *gc.C) { cred3 := dbmodel.CloudCredential{ CloudName: "gce", - OwnerIdentityName: "charlie@external", + OwnerIdentityName: "charlie@canonical.com", Name: "test1", } err = s.JIMM.Database.GetCloudCredential(context.Background(), &cred3) diff --git a/cmd/jimmctl/cmd/importmodel.go b/cmd/jimmctl/cmd/importmodel.go index f97801db9..f50043a9a 100644 --- a/cmd/jimmctl/cmd/importmodel.go +++ b/cmd/jimmctl/cmd/importmodel.go @@ -9,7 +9,7 @@ import ( jujucmd "github.com/juju/juju/cmd" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/api" apiparams "github.com/canonical/jimm/api/params" @@ -24,7 +24,7 @@ const importModelCommandDoc = ` The --owner command is necessary when importing a model created by a local user and it will switch the model owner to the desired external user. - E.g. --owner my-user@external + E.g. --owner my-user@canonical.com Example: jimmctl import-model diff --git a/cmd/jimmctl/cmd/importmodel_test.go b/cmd/jimmctl/cmd/importmodel_test.go index e034b056d..fc221ef55 100644 --- a/cmd/jimmctl/cmd/importmodel_test.go +++ b/cmd/jimmctl/cmd/importmodel_test.go @@ -9,7 +9,7 @@ import ( jjcloud "github.com/juju/juju/cloud" jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/juju/testing/factory" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" @@ -27,7 +27,7 @@ var _ = gc.Suite(&importModelSuite{}) func (s *importModelSuite) TestImportModelSuperuser(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@external/cred") + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@canonical.com/cred") s.UpdateCloudCredential(c, cct, jujuparams.CloudCredential{AuthType: "empty", Attributes: map[string]string{"key": "value"}}) err := s.BackingState.UpdateCloudCredential(cct, jjcloud.NewCredential(jjcloud.EmptyAuthType, map[string]string{"key": "value"})) @@ -35,7 +35,7 @@ func (s *importModelSuite) TestImportModelSuperuser(c *gc.C) { m := s.Factory.MakeModel(c, &factory.ModelParams{ Name: "model-2", - Owner: names.NewUserTag("charlie@external"), + Owner: names.NewUserTag("charlie@canonical.com"), CloudName: jimmtest.TestCloudName, CloudRegion: jimmtest.TestCloudRegionName, CloudCredential: cct, @@ -43,7 +43,7 @@ func (s *importModelSuite) TestImportModelSuperuser(c *gc.C) { defer m.Close() // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") _, err = cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-1", m.ModelUUID()) c.Assert(err, gc.IsNil) @@ -51,17 +51,17 @@ func (s *importModelSuite) TestImportModelSuperuser(c *gc.C) { model2.SetTag(names.NewModelTag(m.ModelUUID())) err = s.JIMM.Database.GetModel(context.Background(), &model2) c.Assert(err, gc.Equals, nil) - c.Check(model2.OwnerIdentityName, gc.Equals, "charlie@external") + c.Check(model2.OwnerIdentityName, gc.Equals, "charlie@canonical.com") } func (s *importModelSuite) TestImportModelFromLocalUser(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@external/cred") + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@canonical.com/cred") s.UpdateCloudCredential(c, cct, jujuparams.CloudCredential{AuthType: "empty"}) // Add credentials for Alice on the test cloud, they are needed for the Alice user to become the new model owner - cctAlice := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/alice@external/cred") + cctAlice := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/alice@canonical.com/cred") s.UpdateCloudCredential(c, cctAlice, jujuparams.CloudCredential{AuthType: "empty"}) - mt := s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) + mt := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) var model dbmodel.Model model.SetTag(mt) err := s.JIMM.Database.GetModel(context.Background(), &model) @@ -70,8 +70,8 @@ func (s *importModelSuite) TestImportModelFromLocalUser(c *gc.C) { c.Assert(err, gc.Equals, nil) // alice is superuser - bClient := s.UserBakeryClient("alice") - _, err = cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-1", mt.Id(), "--owner", "alice@external") + bClient := jimmtest.NewUserSessionLogin("alice") + _, err = cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-1", mt.Id(), "--owner", "alice@canonical.com") c.Assert(err, gc.IsNil) var model2 dbmodel.Model @@ -79,13 +79,13 @@ func (s *importModelSuite) TestImportModelFromLocalUser(c *gc.C) { err = s.JIMM.Database.GetModel(context.Background(), &model2) c.Assert(err, gc.Equals, nil) c.Check(model2.CreatedAt.After(model.CreatedAt), gc.Equals, true) - c.Check(model2.OwnerIdentityName, gc.Equals, "alice@external") + c.Check(model2.OwnerIdentityName, gc.Equals, "alice@canonical.com") } func (s *importModelSuite) TestImportModelUnauthorized(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@external/cred") + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@canonical.com/cred") s.UpdateCloudCredential(c, cct, jujuparams.CloudCredential{AuthType: "empty"}) err := s.BackingState.UpdateCloudCredential(cct, jjcloud.NewCredential(jjcloud.EmptyAuthType, map[string]string{"key": "value"})) @@ -93,7 +93,7 @@ func (s *importModelSuite) TestImportModelUnauthorized(c *gc.C) { m := s.Factory.MakeModel(c, &factory.ModelParams{ Name: "model-2", - Owner: names.NewUserTag("charlie@external"), + Owner: names.NewUserTag("charlie@canonical.com"), CloudName: jimmtest.TestCloudName, CloudRegion: jimmtest.TestCloudRegionName, CloudCredential: cct, @@ -101,31 +101,31 @@ func (s *importModelSuite) TestImportModelUnauthorized(c *gc.C) { defer m.Close() // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err = cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-1", m.ModelUUID()) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *importModelSuite) TestImportModelNoController(c *gc.C) { - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.ErrorMatches, `controller not specified`) } func (s *importModelSuite) TestImportModelNoModelUUID(c *gc.C) { - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-id") c.Assert(err, gc.ErrorMatches, `model uuid not specified`) } func (s *importModelSuite) TestImportModelInvalidModelUUID(c *gc.C) { - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-id", "not-a-uuid") c.Assert(err, gc.ErrorMatches, `invalid model uuid`) } func (s *importModelSuite) TestImportModelTooManyArgs(c *gc.C) { - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-id", "not-a-uuid", "spare-argument") c.Assert(err, gc.ErrorMatches, `too many args`) } diff --git a/cmd/jimmctl/cmd/listauditevents_test.go b/cmd/jimmctl/cmd/listauditevents_test.go index 065060f2a..d66a8d8aa 100644 --- a/cmd/jimmctl/cmd/listauditevents_test.go +++ b/cmd/jimmctl/cmd/listauditevents_test.go @@ -5,7 +5,7 @@ package cmd_test import ( "github.com/juju/cmd/v3/cmdtesting" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" @@ -22,12 +22,12 @@ var _ = gc.Suite(&listAuditEventsSuite{}) func (s *listAuditEventsSuite) TestListAuditEventsSuperuser(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@external/cred") + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@canonical.com/cred") s.UpdateCloudCredential(c, cct, jujuparams.CloudCredential{AuthType: "empty"}) - s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) + s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") context, err := cmdtesting.RunCommand(c, cmd.NewListAuditEventsCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, @@ -36,7 +36,7 @@ func (s *listAuditEventsSuite) TestListAuditEventsSuperuser(c *gc.C) { conversation-id: .* message-id: 1 facade-name: Admin - facade-method: Login + facade-method: LoginWithSessionToken facade-version: \d user-tag: user- is-response: false @@ -46,9 +46,9 @@ func (s *listAuditEventsSuite) TestListAuditEventsSuperuser(c *gc.C) { conversation-id: .* message-id: 1 facade-name: Admin - facade-method: Login + facade-method: LoginWithSessionToken facade-version: \d - user-tag: user- + user-tag: user-alice@canonical.com is-response: true errors: results: @@ -61,12 +61,12 @@ func (s *listAuditEventsSuite) TestListAuditEventsSuperuser(c *gc.C) { func (s *listAuditEventsSuite) TestListAuditEventsStatus(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@external/cred") + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@canonical.com/cred") s.UpdateCloudCredential(c, cct, jujuparams.CloudCredential{AuthType: "empty"}) - s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) + s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewListAuditEventsCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/listcontrollers_test.go b/cmd/jimmctl/cmd/listcontrollers_test.go index fcb1b859a..e22cb9693 100644 --- a/cmd/jimmctl/cmd/listcontrollers_test.go +++ b/cmd/jimmctl/cmd/listcontrollers_test.go @@ -77,7 +77,7 @@ func (s *listControllersSuite) TestListControllersSuperuser(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") context, err := cmdtesting.RunCommand(c, cmd.NewListControllersCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, expectedSuperuserOutput) @@ -87,7 +87,7 @@ func (s *listControllersSuite) TestListControllers(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") context, err := cmdtesting.RunCommand(c, cmd.NewListControllersCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, expectedOutput) diff --git a/cmd/jimmctl/cmd/migratemodel.go b/cmd/jimmctl/cmd/migratemodel.go index 43f78cac6..f17228d62 100644 --- a/cmd/jimmctl/cmd/migratemodel.go +++ b/cmd/jimmctl/cmd/migratemodel.go @@ -11,7 +11,7 @@ import ( jujucmd "github.com/juju/juju/cmd" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/api" apiparams "github.com/canonical/jimm/api/params" diff --git a/cmd/jimmctl/cmd/migratemodel_test.go b/cmd/jimmctl/cmd/migratemodel_test.go index 9eaa1086a..ac7bbec2e 100644 --- a/cmd/jimmctl/cmd/migratemodel_test.go +++ b/cmd/jimmctl/cmd/migratemodel_test.go @@ -5,7 +5,7 @@ package cmd_test import ( "github.com/juju/cmd/v3/cmdtesting" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" @@ -41,13 +41,13 @@ var migrationResultRegex = `results: // but our CLI tests are currently integration based. func (s *migrateModelSuite) TestMigrateModelCommandSuperuser(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@external/cred") + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@canonical.com/cred") s.UpdateCloudCredential(c, cct, jujuparams.CloudCredential{AuthType: "empty"}) - mt := s.AddModel(c, names.NewUserTag("charlie@external"), "model-1", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) - mt2 := s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) + mt := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-1", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) + mt2 := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") context, err := cmdtesting.RunCommand(c, cmd.NewMigrateModelCommandForTesting(s.ClientStore(), bClient), "controller-1", mt.String(), mt2.String()) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, migrationResultRegex) @@ -56,18 +56,18 @@ func (s *migrateModelSuite) TestMigrateModelCommandSuperuser(c *gc.C) { func (s *migrateModelSuite) TestMigrateModelCommandFailsWithInvalidModelTag(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@external/cred") + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@canonical.com/cred") s.UpdateCloudCredential(c, cct, jujuparams.CloudCredential{AuthType: "empty"}) - s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) + s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") _, err := cmdtesting.RunCommand(c, cmd.NewMigrateModelCommandForTesting(s.ClientStore(), bClient), "controller-1", "model-001", "model-002") c.Assert(err, gc.ErrorMatches, ".* is not a valid model tag") } func (s *migrateModelSuite) TestMigrateModelCommandFailsWithMissingArgs(c *gc.C) { - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") _, err := cmdtesting.RunCommand(c, cmd.NewMigrateModelCommandForTesting(s.ClientStore(), bClient), "myController") c.Assert(err, gc.ErrorMatches, "Missing controller and model tag arguments") } diff --git a/cmd/jimmctl/cmd/modelstatus.go b/cmd/jimmctl/cmd/modelstatus.go index 6f434fb91..1ff9bca2d 100644 --- a/cmd/jimmctl/cmd/modelstatus.go +++ b/cmd/jimmctl/cmd/modelstatus.go @@ -9,7 +9,7 @@ import ( jujucmd "github.com/juju/juju/cmd" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/api" apiparams "github.com/canonical/jimm/api/params" diff --git a/cmd/jimmctl/cmd/modelstatus_test.go b/cmd/jimmctl/cmd/modelstatus_test.go index 27dfce70f..fbd85f577 100644 --- a/cmd/jimmctl/cmd/modelstatus_test.go +++ b/cmd/jimmctl/cmd/modelstatus_test.go @@ -5,7 +5,7 @@ package cmd_test import ( "github.com/juju/cmd/v3/cmdtesting" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" @@ -56,12 +56,12 @@ var _ = gc.Suite(&modelStatusSuite{}) func (s *modelStatusSuite) TestModelStatusSuperuser(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@external/cred") + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@canonical.com/cred") s.UpdateCloudCredential(c, cct, jujuparams.CloudCredential{AuthType: "empty", Attributes: map[string]string{"key": "value"}}) - mt := s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) + mt := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") context, err := cmdtesting.RunCommand(c, cmd.NewModelStatusCommandForTesting(s.ClientStore(), bClient), mt.Id()) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, expectedModelStatusOutput) @@ -70,12 +70,12 @@ func (s *modelStatusSuite) TestModelStatusSuperuser(c *gc.C) { func (s *modelStatusSuite) TestModelStatus(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@external/cred") + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@canonical.com/cred") s.UpdateCloudCredential(c, cct, jujuparams.CloudCredential{AuthType: "empty", Attributes: map[string]string{"key": "value"}}) - mt := s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) + mt := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewModelStatusCommandForTesting(s.ClientStore(), bClient), mt.Id()) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/purge_logs_test.go b/cmd/jimmctl/cmd/purge_logs_test.go index 1b20acf5f..f59f1f06b 100644 --- a/cmd/jimmctl/cmd/purge_logs_test.go +++ b/cmd/jimmctl/cmd/purge_logs_test.go @@ -6,12 +6,13 @@ import ( "time" "github.com/juju/cmd/v3/cmdtesting" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/jimmtest" ) type purgeLogsSuite struct { @@ -22,7 +23,7 @@ var _ = gc.Suite(&purgeLogsSuite{}) func (s *purgeLogsSuite) TestPurgeLogsSuperuser(c *gc.C) { // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") datastring := "2021-01-01T00:00:00Z" cmdCtx, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), datastring) c.Assert(err, gc.IsNil) @@ -33,7 +34,7 @@ func (s *purgeLogsSuite) TestPurgeLogsSuperuser(c *gc.C) { func (s *purgeLogsSuite) TestInvalidISO8601Date(c *gc.C) { // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") datastring := "13/01/2021" _, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), datastring) c.Assert(err, gc.ErrorMatches, `invalid date. Expected ISO8601 date`) @@ -42,7 +43,7 @@ func (s *purgeLogsSuite) TestInvalidISO8601Date(c *gc.C) { func (s *purgeLogsSuite) TestPurgeLogs(c *gc.C) { // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), "2021-01-01T00:00:00Z") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } @@ -62,15 +63,15 @@ func (s *purgeLogsSuite) TestPurgeLogsFromDb(c *gc.C) { relativeNow := time.Now().AddDate(-1, 0, 0) ale := dbmodel.AuditLogEntry{ Time: relativeNow.UTC().Round(time.Millisecond), - IdentityTag: names.NewUserTag("alice@external").String(), + IdentityTag: names.NewUserTag("alice@canonical.com").String(), } ale_past := dbmodel.AuditLogEntry{ Time: relativeNow.AddDate(0, 0, -1).UTC().Round(time.Millisecond), - IdentityTag: names.NewUserTag("alice@external").String(), + IdentityTag: names.NewUserTag("alice@canonical.com").String(), } ale_future := dbmodel.AuditLogEntry{ Time: relativeNow.AddDate(0, 0, 5).UTC().Round(time.Millisecond), - IdentityTag: names.NewUserTag("alice@external").String(), + IdentityTag: names.NewUserTag("alice@canonical.com").String(), } err := s.JIMM.Database.Migrate(context.Background(), false) @@ -84,7 +85,7 @@ func (s *purgeLogsSuite) TestPurgeLogsFromDb(c *gc.C) { tomorrow := relativeNow.AddDate(0, 0, 1).Format(layout) //alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") cmdCtx, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), tomorrow) c.Assert(err, gc.IsNil) // check that logs have been deleted diff --git a/cmd/jimmctl/cmd/relation.go b/cmd/jimmctl/cmd/relation.go index 20f36414c..2bb087b99 100644 --- a/cmd/jimmctl/cmd/relation.go +++ b/cmd/jimmctl/cmd/relation.go @@ -118,7 +118,7 @@ jimmctl auth relation remove group-MyTeam#member loginer controller-MyController Verifies the access between resources. Example: -jimmctl auth relation check user-alice@external administrator controller-aws-controller-1 +jimmctl auth relation check user-alice@canonical.com administrator controller-aws-controller-1 Example: jimmctl auth relation check diff --git a/cmd/jimmctl/cmd/relation_test.go b/cmd/jimmctl/cmd/relation_test.go index 7da41f007..1e8e48d22 100644 --- a/cmd/jimmctl/cmd/relation_test.go +++ b/cmd/jimmctl/cmd/relation_test.go @@ -15,7 +15,7 @@ import ( petname "github.com/dustinkirkland/golang-petname" "github.com/google/uuid" "github.com/juju/cmd/v3/cmdtesting" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" yamlv2 "gopkg.in/yaml.v2" "gopkg.in/yaml.v3" @@ -25,6 +25,7 @@ import ( "github.com/canonical/jimm/internal/cmdtest" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/jimmtest" "github.com/canonical/jimm/internal/openfga" ofganames "github.com/canonical/jimm/internal/openfga/names" jimmnames "github.com/canonical/jimm/pkg/names" @@ -38,7 +39,7 @@ var _ = gc.Suite(&relationSuite{}) func (s *relationSuite) TestAddRelationSuperuser(c *gc.C) { // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") group1 := "testGroup1" group2 := "testGroup2" type tuple struct { @@ -109,7 +110,7 @@ func (s *relationSuite) TestAddRelationSuperuser(c *gc.C) { func (s *relationSuite) TestMissingParamsAddRelationSuperuser(c *gc.C) { // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") _, err := cmdtesting.RunCommand(c, cmd.NewAddRelationCommandForTesting(s.ClientStore(), bClient), "foo", "bar") c.Assert(err, gc.ErrorMatches, "target object not specified") @@ -122,7 +123,7 @@ func (s *relationSuite) TestMissingParamsAddRelationSuperuser(c *gc.C) { func (s *relationSuite) TestAddRelationViaFileSuperuser(c *gc.C) { // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") group1 := "testGroup1" group2 := "testGroup2" group3 := "testGroup3" @@ -151,14 +152,14 @@ func (s *relationSuite) TestAddRelationViaFileSuperuser(c *gc.C) { } func (s *relationSuite) TestAddRelationRejectsUnauthorisedUsers(c *gc.C) { - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewAddRelationCommandForTesting(s.ClientStore(), bClient), "test-group1", "member", "test-group2") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *relationSuite) TestRemoveRelationSuperuser(c *gc.C) { // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") group1 := "testGroup1" group2 := "testGroup2" type tuple struct { @@ -204,7 +205,7 @@ func (s *relationSuite) TestRemoveRelationSuperuser(c *gc.C) { } func (s *relationSuite) TestRemoveRelationViaFileSuperuser(c *gc.C) { - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") group1 := "testGroup1" group2 := "testGroup2" group3 := "testGroup3" @@ -242,7 +243,7 @@ func (s *relationSuite) TestRemoveRelationViaFileSuperuser(c *gc.C) { Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewControllerTag(s.Params.ControllerUUID)), }, { - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewControllerTag(s.Params.ControllerUUID)), }}) @@ -250,7 +251,7 @@ func (s *relationSuite) TestRemoveRelationViaFileSuperuser(c *gc.C) { func (s *relationSuite) TestRemoveRelation(c *gc.C) { // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewRemoveRelationCommandForTesting(s.ClientStore(), bClient), "test-group1#member", "member", "test-group2") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } @@ -268,7 +269,7 @@ func initializeEnvironment(c *gc.C, ctx context.Context, db *db.Database, u dbmo env := environment{} u1 := dbmodel.Identity{ - Name: "eve@external", + Name: "eve@canonical.com", } c.Assert(db.DB.Create(&u1).Error, gc.IsNil) @@ -345,7 +346,7 @@ func initializeEnvironment(c *gc.C, ctx context.Context, db *db.Database, u dbmo func (s *relationSuite) TestListRelations(c *gc.C) { env := initializeEnvironment(c, context.Background(), &s.JIMM.Database, *s.AdminUser) // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") groups := []string{"group-1", "group-2", "group-3"} for _, group := range groups { @@ -389,7 +390,7 @@ func (s *relationSuite) TestListRelations(c *gc.C) { Relation: "administrator", TargetObject: "controller-jimm", }, { - Object: "user-alice@external", + Object: "user-alice@canonical.com", Relation: "administrator", TargetObject: "controller-jimm", }}, @@ -402,7 +403,7 @@ func (s *relationSuite) TestListRelations(c *gc.C) { Relation: "administrator", TargetObject: "controller-jimm", }, { - Object: "user-alice@external", + Object: "user-alice@canonical.com", Relation: "administrator", TargetObject: "controller-jimm", }}, @@ -423,22 +424,22 @@ func (s *relationSuite) TestListRelations(c *gc.C) { c.Assert( cmdtesting.Stdout(context), gc.Equals, - `Object Relation Target Object -user-admin administrator controller-jimm -user-alice@external administrator controller-jimm -user-alice@external member group-group-1 -user-eve@external member group-group-2 -group-group-2#member member group-group-3 -group-group-3#member administrator controller-test-controller-1 -group-group-1#member administrator model-test-controller-1:alice@external/test-model-1 -user-eve@external administrator applicationoffer-test-controller-1:alice@external/test-model-1.testoffer1`, + `Object Relation Target Object +user-admin administrator controller-jimm +user-alice@canonical.com administrator controller-jimm +user-alice@canonical.com member group-group-1 +user-eve@canonical.com member group-group-2 +group-group-2#member member group-group-3 +group-group-3#member administrator controller-test-controller-1 +group-group-1#member administrator model-test-controller-1:alice@canonical.com/test-model-1 +user-eve@canonical.com administrator applicationoffer-test-controller-1:alice@canonical.com/test-model-1.testoffer1`, ) } // TODO: remove boilerplate of env setup and use initialiseEnvironment func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { ctx := context.TODO() - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") ofgaClient := s.JIMM.OpenFGAClient // Add some resources to check against @@ -450,7 +451,7 @@ func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { c.Assert(err, gc.IsNil) u := dbmodel.Identity{ - Name: petname.Generate(2, "-") + "@external", + Name: petname.Generate(2, "-") + "@canonical.com", } c.Assert(db.DB.Create(&u).Error, gc.IsNil) @@ -613,7 +614,7 @@ func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { func (s *relationSuite) TestCheckRelation(c *gc.C) { // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand( c, cmd.NewCheckRelationCommandForTesting(s.ClientStore(), bClient), diff --git a/cmd/jimmctl/cmd/removecloudfromcontroller.go b/cmd/jimmctl/cmd/removecloudfromcontroller.go index 76a316a47..1e8ddb015 100644 --- a/cmd/jimmctl/cmd/removecloudfromcontroller.go +++ b/cmd/jimmctl/cmd/removecloudfromcontroller.go @@ -11,7 +11,7 @@ import ( jujucmd "github.com/juju/juju/cmd" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/api" apiparams "github.com/canonical/jimm/api/params" diff --git a/cmd/jimmctl/cmd/removecloudfromcontroller_test.go b/cmd/jimmctl/cmd/removecloudfromcontroller_test.go index f3c693346..038de08c5 100644 --- a/cmd/jimmctl/cmd/removecloudfromcontroller_test.go +++ b/cmd/jimmctl/cmd/removecloudfromcontroller_test.go @@ -10,6 +10,7 @@ import ( apiparams "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/cmd/jimmctl/cmd" "github.com/canonical/jimm/internal/cmdtest" + "github.com/canonical/jimm/internal/jimmtest" ) type removeCloudFromControllerSuite struct { @@ -26,7 +27,7 @@ func (s *removeCloudFromControllerSuite) SetUpTest(c *gc.C) { } func (s *removeCloudFromControllerSuite) TestRemoveCloudFromController(c *gc.C) { - bClient := s.UserBakeryClient("alice@external") + bClient := jimmtest.NewUserSessionLogin("alice@canonical.com") command := cmd.NewRemoveCloudFromControllerCommandForTesting( s.ClientStore(), @@ -48,7 +49,7 @@ func (s *removeCloudFromControllerSuite) TestRemoveCloudFromController(c *gc.C) } func (s *removeCloudFromControllerSuite) TestRemoveCloudFromControllerWrongArguments(c *gc.C) { - bClient := s.UserBakeryClient("alice@external") + bClient := jimmtest.NewUserSessionLogin("alice@canonical.com") command := cmd.NewRemoveCloudFromControllerCommandForTesting( s.ClientStore(), @@ -63,7 +64,7 @@ func (s *removeCloudFromControllerSuite) TestRemoveCloudFromControllerWrongArgum } func (s *removeCloudFromControllerSuite) TestRemoveCloudFromControllerCloudNotFound(c *gc.C) { - bClient := s.UserBakeryClient("alice@external") + bClient := jimmtest.NewUserSessionLogin("alice@canonical.com") command := cmd.NewRemoveCloudFromControllerCommandForTesting( s.ClientStore(), diff --git a/cmd/jimmctl/cmd/removecontroller_test.go b/cmd/jimmctl/cmd/removecontroller_test.go index 5f6f2578f..be1226a6f 100644 --- a/cmd/jimmctl/cmd/removecontroller_test.go +++ b/cmd/jimmctl/cmd/removecontroller_test.go @@ -21,7 +21,7 @@ func (s *removeControllerSuite) TestRemoveControllerSuperuser(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") context, err := cmdtesting.RunCommand(c, cmd.NewRemoveControllerCommandForTesting(s.ClientStore(), bClient), "controller-1", "--force") c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, `name: controller-1 @@ -70,7 +70,7 @@ func (s *removeControllerSuite) TestRemoveController(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewRemoveControllerCommandForTesting(s.ClientStore(), bClient), "controller-1", "--force") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/revokeauditlogaccess.go b/cmd/jimmctl/cmd/revokeauditlogaccess.go index b215105fa..4175b916c 100644 --- a/cmd/jimmctl/cmd/revokeauditlogaccess.go +++ b/cmd/jimmctl/cmd/revokeauditlogaccess.go @@ -9,7 +9,7 @@ import ( jujucmd "github.com/juju/juju/cmd" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/api" apiparams "github.com/canonical/jimm/api/params" diff --git a/cmd/jimmctl/cmd/revokeauditlogaccess_test.go b/cmd/jimmctl/cmd/revokeauditlogaccess_test.go index f4b20f372..ccc5ad17b 100644 --- a/cmd/jimmctl/cmd/revokeauditlogaccess_test.go +++ b/cmd/jimmctl/cmd/revokeauditlogaccess_test.go @@ -8,6 +8,7 @@ import ( "github.com/canonical/jimm/cmd/jimmctl/cmd" "github.com/canonical/jimm/internal/cmdtest" + "github.com/canonical/jimm/internal/jimmtest" ) type revokeAuditLogAccessSuite struct { @@ -19,14 +20,14 @@ type revokeAuditLogAccessSuite struct { func (s *revokeAuditLogAccessSuite) TestRevokeAuditLogAccessSuperuser(c *gc.C) { // alice is superuser - bClient := s.UserBakeryClient("alice") - _, err := cmdtesting.RunCommand(c, cmd.NewRevokeAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@external") + bClient := jimmtest.NewUserSessionLogin("alice") + _, err := cmdtesting.RunCommand(c, cmd.NewRevokeAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@canonical.com") c.Assert(err, gc.IsNil) } func (s *revokeAuditLogAccessSuite) TestRevokeAuditLogAccess(c *gc.C) { // bob is not superuser - bClient := s.UserBakeryClient("bob") - _, err := cmdtesting.RunCommand(c, cmd.NewRevokeAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@external") + bClient := jimmtest.NewUserSessionLogin("bob") + _, err := cmdtesting.RunCommand(c, cmd.NewRevokeAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@canonical.com") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go b/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go index 1afba77f7..4d70ce69e 100644 --- a/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go +++ b/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go @@ -21,7 +21,7 @@ func (s *setControllerDeprecatedSuite) TestSetControllerDeprecatedSuperuser(c *g s.AddController(c, "controller-1", s.APIInfo(c)) // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") context, err := cmdtesting.RunCommand(c, cmd.NewSetControllerDeprecatedCommandForTesting(s.ClientStore(), bClient), "controller-1") c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, `name: controller-1 @@ -70,7 +70,7 @@ func (s *setControllerDeprecatedSuite) TestSetControllerDeprecated(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewSetControllerDeprecatedCommandForTesting(s.ClientStore(), bClient), "controller-1") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/updatemigratedmodel.go b/cmd/jimmctl/cmd/updatemigratedmodel.go index 92fde9ee6..89d56f1fa 100644 --- a/cmd/jimmctl/cmd/updatemigratedmodel.go +++ b/cmd/jimmctl/cmd/updatemigratedmodel.go @@ -8,7 +8,7 @@ import ( jujucmd "github.com/juju/juju/cmd" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/api" apiparams "github.com/canonical/jimm/api/params" diff --git a/cmd/jimmctl/cmd/updatemigratedmodel_test.go b/cmd/jimmctl/cmd/updatemigratedmodel_test.go index 26e5db7cb..7a6fe4ebe 100644 --- a/cmd/jimmctl/cmd/updatemigratedmodel_test.go +++ b/cmd/jimmctl/cmd/updatemigratedmodel_test.go @@ -7,7 +7,7 @@ import ( "github.com/juju/cmd/v3/cmdtesting" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/cmd/jimmctl/cmd" @@ -25,9 +25,9 @@ var _ = gc.Suite(&updateMigratedModelSuite{}) func (s *updateMigratedModelSuite) TestUpdateMigratedModelSuperuser(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@external/cred") + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@canonical.com/cred") s.UpdateCloudCredential(c, cct, jujuparams.CloudCredential{AuthType: "empty"}) - mt := s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) + mt := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) var model dbmodel.Model model.SetTag(mt) err := s.JIMM.Database.GetModel(context.Background(), &model) @@ -35,7 +35,7 @@ func (s *updateMigratedModelSuite) TestUpdateMigratedModelSuperuser(c *gc.C) { s.AddController(c, "controller-2", s.APIInfo(c)) // alice is superuser - bClient := s.UserBakeryClient("alice") + bClient := jimmtest.NewUserSessionLogin("alice") _, err = cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-2", mt.Id()) c.Assert(err, gc.IsNil) @@ -50,36 +50,36 @@ func (s *updateMigratedModelSuite) TestUpdateMigratedModelSuperuser(c *gc.C) { func (s *updateMigratedModelSuite) TestUpdateMigratedModelUnauthorized(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@external/cred") + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@canonical.com/cred") s.UpdateCloudCredential(c, cct, jujuparams.CloudCredential{AuthType: "empty"}) - mt := s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) + mt := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // bob is not superuser - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-1", mt.Id()) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *updateMigratedModelSuite) TestUpdateMigratedModelNoController(c *gc.C) { - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.ErrorMatches, `controller not specified`) } func (s *updateMigratedModelSuite) TestUpdateMigratedModelNoModelUUID(c *gc.C) { - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-id") c.Assert(err, gc.ErrorMatches, `model uuid not specified`) } func (s *updateMigratedModelSuite) TestUpdateMigratedModelInvalidModelUUID(c *gc.C) { - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-id", "not-a-uuid") c.Assert(err, gc.ErrorMatches, `invalid model uuid`) } func (s *updateMigratedModelSuite) TestUpdateMigratedModelTooManyArgs(c *gc.C) { - bClient := s.UserBakeryClient("bob") + bClient := jimmtest.NewUserSessionLogin("bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-id", "not-a-uuid", "spare-argument") c.Assert(err, gc.ErrorMatches, `too many args`) } diff --git a/cmd/jimmsrv/main.go b/cmd/jimmsrv/main.go index 79c5b052f..37079c200 100644 --- a/cmd/jimmsrv/main.go +++ b/cmd/jimmsrv/main.go @@ -137,9 +137,6 @@ func start(ctx context.Context, s *service.Service) error { jimmsvc, err := jimm.NewService(ctx, jimm.Params{ ControllerUUID: os.Getenv("JIMM_UUID"), DSN: os.Getenv("JIMM_DSN"), - CandidURL: os.Getenv("CANDID_URL"), - CandidPublicKey: os.Getenv("CANDID_PUBLIC_KEY"), - BakeryAgentFile: os.Getenv("BAKERY_AGENT_FILE"), ControllerAdmins: strings.Fields(os.Getenv("JIMM_ADMINS")), VaultSecretFile: os.Getenv("VAULT_SECRET_FILE"), VaultAddress: os.Getenv("VAULT_ADDR"), diff --git a/docker-compose.yaml b/docker-compose.yaml index 5303fd933..9a52e2648 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -44,9 +44,6 @@ services: JIMM_LOG_LEVEL: "debug" JIMM_UUID: "3217dbc9-8ea9-4381-9e97-01eab0b3f6bb" JIMM_DSN: "postgresql://jimm:jimm@db/jimm" - CANDID_URL: "http://0.0.0.0:8081" # For external client redirects (in the case of compose and running outside) - # Hardcoded pre-generated key, see ./local/candid/config.yaml - CANDID_PUBLIC_KEY: "CIdWcEUN+0OZnKW9KwruRQnQDY/qqzVdD30CijwiWCk=" # Not needed for local test (yet). # BAKERY_AGENT_FILE: "" JIMM_ADMINS: "jimm@candid.localhost" @@ -154,27 +151,6 @@ services: db: condition: service_healthy - candid: - image: candid:latest - container_name: candid - entrypoint: "/candid.sh" - profiles: ["dev"] - expose: - - 8081 - ports: - - 8081:8081 - volumes: - - ./local/candid/config.yaml:/etc/candid/config.yaml - - ./local/candid/entry.sh:/candid.sh - depends_on: - db: - condition: service_healthy - healthcheck: - test: [ "CMD", "curl", "http://localhost:8081/debug/status" ] - interval: 5s - timeout: 5s - retries: 5 - migrateopenfga: image: openfga/openfga:v1.2.0 container_name: migrateopenfga diff --git a/go.mod b/go.mod index de3617fde..d931672c5 100644 --- a/go.mod +++ b/go.mod @@ -3,23 +3,22 @@ module github.com/canonical/jimm go 1.21.3 require ( - github.com/canonical/candid v1.12.2 - github.com/canonical/go-dqlite v1.20.0 // indirect + github.com/canonical/go-dqlite v1.21.0 // indirect github.com/canonical/go-service v1.0.0 github.com/frankban/quicktest v1.14.6 github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1 github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe // indirect - github.com/google/go-cmp v0.5.9 - github.com/google/uuid v1.3.0 - github.com/gorilla/websocket v1.5.0 + github.com/google/go-cmp v0.6.0 + github.com/google/uuid v1.5.0 + github.com/gorilla/websocket v1.5.1 github.com/gosuri/uitable v0.0.4 - github.com/hashicorp/vault/api v1.8.2 + github.com/hashicorp/vault/api v1.10.0 github.com/jackc/pgconn v1.13.0 github.com/jackc/pgx/v4 v4.17.2 github.com/juju/cmd/v3 v3.0.14 github.com/juju/errors v1.0.0 github.com/juju/gnuflag v1.0.0 - github.com/juju/juju v0.0.0-20231110092546-f835120f6974 + github.com/juju/juju v0.0.0-20240304110523-55fb5d03683b github.com/juju/loggo v1.0.0 github.com/juju/mgomonitor v0.0.0-20181029151116-52206bb0cd31 github.com/juju/names/v4 v4.0.0 @@ -31,10 +30,10 @@ require ( github.com/juju/zaputil v0.0.0-20190326175239-ef53049637ac github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/openfga/go-sdk v0.2.2 - github.com/prometheus/client_golang v1.14.0 + github.com/prometheus/client_golang v1.18.0 github.com/rogpeppe/fastuuid v1.2.0 go.uber.org/zap v1.24.0 - golang.org/x/net v0.17.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/sync v0.5.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/macaroon-bakery.v2 v2.3.0 @@ -45,19 +44,21 @@ require ( ) require ( + github.com/antonlindstrom/pgstore v0.0.0-20220421113606-e3a6e3fed12a github.com/canonical/ofga v0.10.0 github.com/coreos/go-oidc/v3 v3.9.0 - github.com/dustinkirkland/golang-petname v0.0.0-20230626224747-e794b9370d49 + github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/render v1.0.2 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/itchyny/gojq v0.12.12 - github.com/juju/charm/v11 v11.0.2 + github.com/juju/charm/v12 v12.0.0 + github.com/juju/names/v5 v5.0.0 github.com/lestrrat-go/iter v1.0.2 - github.com/lestrrat-go/jwx/v2 v2.0.11 + github.com/lestrrat-go/jwx/v2 v2.0.19 github.com/oklog/ulid/v2 v2.1.0 github.com/stretchr/testify v1.8.4 - golang.org/x/oauth2 v0.13.0 + golang.org/x/oauth2 v0.15.0 gopkg.in/errgo.v1 v1.0.1 gopkg.in/httprequest.v1 v1.2.1 gopkg.in/yaml.v2 v2.4.0 @@ -65,122 +66,99 @@ require ( ) require ( - cloud.google.com/go/compute v1.20.1 // indirect + cloud.google.com/go/compute v1.23.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2 v2.0.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.2.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.4.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.2.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect github.com/Rican7/retry v0.3.1 // indirect github.com/adrg/xdg v0.3.3 // indirect github.com/ajg/form v1.5.1 // indirect - github.com/antonlindstrom/pgstore v0.0.0-20220421113606-e3a6e3fed12a // indirect - github.com/armon/go-metrics v0.4.0 // indirect - github.com/armon/go-radix v1.0.0 // indirect - github.com/aws/aws-sdk-go-v2 v1.21.0 // indirect - github.com/aws/aws-sdk-go-v2/config v1.18.35 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.13.35 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 // indirect - github.com/aws/aws-sdk-go-v2/service/ec2 v1.113.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ecr v1.19.2 // indirect - github.com/aws/aws-sdk-go-v2/service/iam v1.22.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.13.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 // indirect - github.com/aws/smithy-go v1.14.2 // indirect + github.com/aws/aws-sdk-go-v2 v1.24.0 // indirect + github.com/aws/aws-sdk-go-v2/config v1.26.2 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.13 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ec2 v1.142.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.24.6 // indirect + github.com/aws/aws-sdk-go-v2/service/iam v1.28.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.26.6 // indirect + github.com/aws/smithy-go v1.19.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bgentry/speakeasy v0.1.0 // indirect - github.com/bmizerany/pat v0.0.0-20160217103242-c068ca2f0aac // indirect - github.com/canonical/lxd v0.0.0-20230712132802-8d2a42545fd0 // indirect + github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f // indirect + github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8 // indirect + github.com/canonical/lxd v0.0.0-20231214113525-e676fc63c50a // indirect + github.com/canonical/pebble v1.9.0 // indirect + github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b // indirect github.com/cenkalti/backoff/v3 v3.0.0 // indirect - github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cjlapao/common-go v0.0.39 // indirect - github.com/cloudflare/cfssl v1.6.1 // indirect - github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe // indirect - github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect - github.com/coreos/go-semver v0.3.0 // indirect - github.com/coreos/go-systemd/v22 v22.3.2 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/creack/pty v1.1.15 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/duo-labs/webauthn v0.0.0-20220815211337-00c9fb5711f5 // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f // indirect - github.com/envoyproxy/protoc-gen-validate v0.10.1 // indirect - github.com/fatih/color v1.13.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fatih/color v1.14.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 // indirect - github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/fullstorydev/grpcurl v1.8.1 // indirect - github.com/fxamacker/cbor/v2 v2.4.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/tcell/v2 v2.5.1 // indirect github.com/go-goose/goose/v5 v5.0.0-20230421180421-abaee9096e3a // indirect github.com/go-jose/go-jose/v3 v3.0.1 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/godbus/dbus/v5 v5.0.4 // indirect github.com/gofrs/flock v0.8.1 // indirect - github.com/gofrs/uuid v4.2.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/btree v1.0.1 // indirect - github.com/google/certificate-transparency-go v1.1.2-0.20210511102531-373a877eec92 // indirect + github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/renameio v1.0.1 // indirect - github.com/google/s2a-go v0.1.4 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect - github.com/googleapis/gax-go/v2 v2.11.0 // indirect - github.com/googleapis/gnostic v0.5.5 // indirect - github.com/gorilla/schema v1.2.0 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/schema v1.2.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.2.1 // indirect - github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect - github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect - github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-hclog v1.2.0 // indirect - github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.4.5 // indirect github.com/hashicorp/go-retryablehttp v0.6.6 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect - github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-sockaddr v1.0.2 // indirect - github.com/hashicorp/go-uuid v1.0.2 // indirect - github.com/hashicorp/go-version v1.3.0 // indirect - github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hashicorp/vault/sdk v0.6.0 // indirect - github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect github.com/im7mortal/kmutex v1.0.1 // indirect github.com/imdario/mergo v0.3.12 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/timefmt-go v0.1.5 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgio v1.0.0 // indirect @@ -188,18 +166,16 @@ require ( github.com/jackc/pgproto3/v2 v2.3.1 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/jackc/pgtype v1.12.0 // indirect - github.com/jhump/protoreflect v1.8.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/jonboulle/clockwork v0.2.2 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/juju/aclstore/v2 v2.1.0 // indirect github.com/juju/ansiterm v1.0.0 // indirect github.com/juju/blobstore/v3 v3.0.2 // indirect github.com/juju/clock v1.0.3 // indirect github.com/juju/collections v1.0.4 // indirect - github.com/juju/description/v4 v4.0.11 // indirect + github.com/juju/description/v5 v5.0.0 // indirect github.com/juju/featureflag v1.0.0 // indirect github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a // indirect github.com/juju/gojsonpointer v0.0.0-20150204194629-afe8b77aa08f // indirect @@ -207,7 +183,6 @@ require ( github.com/juju/gojsonschema v1.0.0 // indirect github.com/juju/gomaasapi/v2 v2.2.0 // indirect github.com/juju/http/v2 v2.0.0 // indirect - github.com/juju/httpprof v0.0.0-20141217160036-14bf14c30767 // indirect github.com/juju/idmclient/v2 v2.0.0 // indirect github.com/juju/jsonschema v1.0.0 // indirect github.com/juju/lru v1.0.0 // indirect @@ -226,22 +201,20 @@ require ( github.com/juju/retry v1.0.0 // indirect github.com/juju/rfc/v2 v2.0.0 // indirect github.com/juju/romulus v1.0.0 // indirect - github.com/juju/schema v1.0.1 // indirect - github.com/juju/simplekv v1.1.0 // indirect + github.com/juju/schema v1.2.0 // indirect github.com/juju/txn/v3 v3.0.2 // indirect github.com/juju/usso v1.0.1 // indirect - github.com/juju/utils v0.0.0-20200604140309-9d78121a29e0 // indirect - github.com/juju/utils/v3 v3.0.2 // indirect + github.com/juju/utils/v3 v3.1.0 // indirect github.com/juju/viddy v0.0.0-beta5 // indirect github.com/juju/webbrowser v1.0.0 // indirect - github.com/juju/worker/v3 v3.4.0 // indirect + github.com/juju/worker/v3 v3.5.0 // indirect github.com/julienschmidt/httprouter v1.3.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/fs v0.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/lestrrat-go/blackmagic v1.0.1 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/option v1.0.1 // indirect @@ -255,111 +228,91 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect - github.com/microsoft/kiota-abstractions-go v1.2.0 // indirect - github.com/microsoft/kiota-authentication-azure-go v1.0.0 // indirect - github.com/microsoft/kiota-http-go v1.0.1 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/microsoft/kiota-abstractions-go v1.5.3 // indirect + github.com/microsoft/kiota-authentication-azure-go v1.0.1 // indirect + github.com/microsoft/kiota-http-go v1.1.1 // indirect github.com/microsoft/kiota-serialization-form-go v1.0.0 // indirect github.com/microsoft/kiota-serialization-json-go v1.0.4 // indirect + github.com/microsoft/kiota-serialization-multipart-go v1.0.0 // indirect github.com/microsoft/kiota-serialization-text-go v1.0.0 // indirect - github.com/microsoftgraph/msgraph-sdk-go v1.14.0 // indirect - github.com/microsoftgraph/msgraph-sdk-go-core v1.0.0 // indirect - github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/microsoftgraph/msgraph-sdk-go v1.28.0 // indirect + github.com/microsoftgraph/msgraph-sdk-go-core v1.0.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb // indirect - github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/mitchellh/reflectwalk v1.0.1 // indirect - github.com/mittwald/vaultgo v0.1.1 // indirect + github.com/mittwald/vaultgo v0.1.4 // indirect github.com/moby/spdystream v0.2.0 // indirect - github.com/moby/sys/mountinfo v0.6.2 // indirect + github.com/moby/sys/mountinfo v0.7.1 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/muhlemmer/gu v0.3.1 // indirect - github.com/oklog/run v1.0.0 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/oracle/oci-go-sdk/v65 v65.49.0 // indirect + github.com/oracle/oci-go-sdk/v65 v65.55.0 // indirect github.com/packethost/packngo v0.28.1 // indirect - github.com/pborman/uuid v1.2.1 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/pierrec/lz4 v2.6.1+incompatible // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pkg/sftp v1.13.5 // indirect + github.com/pkg/sftp v1.13.6 // indirect + github.com/pkg/term v1.1.0 // indirect github.com/pkg/xattr v0.4.9 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.11.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect github.com/rivo/tview v0.0.0-20220610163003-691f46d6f500 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rs/xid v1.5.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/soheilhy/cmux v0.1.5 // indirect github.com/sony/gobreaker v0.5.0 // indirect - github.com/spf13/afero v1.9.5 // indirect - github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/cobra v1.7.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.16.0 // indirect - github.com/subosito/gotenv v1.4.2 // indirect - github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect - github.com/urfave/cli v1.22.5 // indirect + github.com/spf13/viper v1.17.0 // indirect + github.com/std-uritemplate/std-uritemplate/go v0.0.47 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/vishvananda/netlink v1.2.1-beta.2 // indirect github.com/vishvananda/netns v0.0.4 // indirect - github.com/vmware/govmomi v0.21.1-0.20191008161538-40aebf13ba45 // indirect - github.com/x448/float16 v0.8.4 // indirect + github.com/vmware/govmomi v0.34.1 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect - github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect - github.com/yohcop/openid-go v1.0.0 // indirect - github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - github.com/zitadel/oidc/v2 v2.6.4 // indirect - go.etcd.io/bbolt v1.3.5 // indirect - go.etcd.io/etcd/api/v3 v3.5.9 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.9 // indirect - go.etcd.io/etcd/client/v2 v2.305.7 // indirect - go.etcd.io/etcd/client/v3 v3.5.9 // indirect - go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0 // indirect - go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0 // indirect - go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0 // indirect - go.etcd.io/etcd/server/v3 v3.5.0-alpha.0 // indirect - go.etcd.io/etcd/tests/v3 v3.5.0-alpha.0 // indirect - go.etcd.io/etcd/v3 v3.5.0-alpha.0 // indirect + github.com/zitadel/oidc/v2 v2.12.0 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/otel v1.16.0 // indirect - go.opentelemetry.io/otel/metric v1.16.0 // indirect - go.opentelemetry.io/otel/trace v1.16.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/mock v0.2.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - golang.org/x/crypto v0.16.0 // indirect - golang.org/x/mod v0.13.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/term v0.15.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect + go.opentelemetry.io/otel v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/mock v0.4.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.14.0 // indirect - google.golang.org/api v0.126.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.16.1 // indirect + google.golang.org/api v0.154.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect - google.golang.org/grpc v1.56.3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect + google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.31.0 // indirect - gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect gopkg.in/gobwas/glob.v0 v0.2.3 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect @@ -369,15 +322,15 @@ require ( gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect - k8s.io/api v0.23.4 // indirect - k8s.io/apiextensions-apiserver v0.21.10 // indirect - k8s.io/apimachinery v0.23.4 // indirect - k8s.io/client-go v0.23.4 // indirect - k8s.io/klog/v2 v2.80.1 // indirect - k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect - k8s.io/utils v0.0.0-20230711102312-30195339c3c7 // indirect - sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect + k8s.io/api v0.29.0 // indirect + k8s.io/apiextensions-apiserver v0.29.0 // indirect + k8s.io/apimachinery v0.29.0 // indirect + k8s.io/client-go v0.29.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) replace ( diff --git a/go.sum b/go.sum index dbc6fe20c..b53d035bd 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,8 @@ -bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= -bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= -bitbucket.org/creachadair/shell v0.0.6/go.mod h1:8Qqi/cYk7vPnsOePHroKXDJYmb5x7ENhtiFtfZq8K+M= -bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -21,7 +15,6 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= @@ -31,8 +24,8 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= -cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= @@ -42,559 +35,223 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/spanner v1.17.0/go.mod h1:+17t2ixFwRG4lWRwE+5kipDR9Ef07Jkmc8z0IbMDKUs= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -code.gitea.io/sdk/gitea v0.11.3/go.mod h1:z3uwDV/b9Ls47NGukYM9XhnHtqPh/J+t40lsUrR6JDY= -contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= -contrib.go.opencensus.io/exporter/ocagent v0.5.0/go.mod h1:ImxhfLRpxoYiSq891pBrLVhN+qmP8BTVvdH2YLs7Gl0= -contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw= -contrib.go.opencensus.io/exporter/stackdriver v0.13.5/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= -contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= -contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-amqp-common-go/v2 v2.1.0/go.mod h1:R8rea+gJRuJR6QxTir/XuEd+YuKoUiazDC/N96FiDEU= -github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= -github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v29.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v30.1.0+incompatible h1:HyYPft8wXpxMd0kfLtXo6etWcO+XuPbLkcgx9g2cqxU= -github.com/Azure/azure-sdk-for-go v30.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 h1:/iHxaJhsFr0+xVFfbMr5vxz848jyiWuIEDhYq3y5odY= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.1 h1:kRt6idL93W/nYRkUPbZ81yxJeLFevvrLYkyJEVzLpYM= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.1/go.mod h1:nPsyC5G3IY+ljp+OHp8w/xa9UuLWe7ehFADNkqCSTaw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 h1:qiir/pptnHqp6hV8QwV+IExYIf6cPsXBfUDUXQ27t2Y= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2/go.mod h1:jVRrRDLCOuif95HDYC23ADTMlvahB7tMdl519m9Iyjc= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0 h1:/Di3vB4sNeQ+7A8efjUVENvyB945Wruvstucqp7ZArg= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0/go.mod h1:gM3K25LQlsET3QR+4V74zxCsFAy0r6xMNN9n80SZn+4= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2 v2.0.0 h1:xxe4naFUPYEW1W6C8yWrfFNmyZLnEbO+CsbsSF83wDo= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2 v2.0.0/go.mod h1:aLFjumYDvv63tH1qnqkcmdjdZ6Sn+/viPv7H3jft0oY= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2/go.mod h1:FbdwsQ2EzwvXxOPcMFYO8ogEc9uMMIj3YkmCdXdAFmk= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.2.0 h1:8d4U82r7ItT1Es91x3eUcAQweih36KWvUha8AZ9X0Rs= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.2.0/go.mod h1:/1bkGperHinQbAHMWivoec/Ucu6//iXo6jn5mhmqCVU= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.0.0 h1:lMW1lD/17LUA5z1XTURo7LcVG2ICBPlyMHjIUrcFZNQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.0.0/go.mod h1:ceIuwmxDWptoW3eCqSXlnPsZFKh4X+R38dWPv7GS9Vs= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.4.0 h1:HlZMUZW8S4P9oob1nCHxCCKrytxyLc+24nUJGssoEto= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.4.0/go.mod h1:StGsLbuJh06Bd8IBfnAlIFV3fLb+gkczONWf15hpX2E= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0 h1:QM6sE5k2ZT/vI5BEe0r7mqjsUSnhVBFbOsVkEuaEfiA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0/go.mod h1:243D9iHbcQXoFUtgHJwL7gl2zx1aDuDMjvBZVGr2uW0= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.2.0 h1:Pmy0+3ox1IC3sp6musv87BFPIdQbqyPFjn7I8I0o2Js= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.2.0/go.mod h1:ThfyMjs6auYrWPnYJjI3H4H++oVPrz01pizpu8lfl3A= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 h1:yfJe15aSwEQ6Oo6J+gdfdulPNoZ3TEhmbhLIoxZcA+U= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 h1:T028gtTPiYt/RMUfs8nVsAL7FDQrfLlrm/NnRG/zcC4= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= -github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0= -github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 h1:wxQx2Bt4xzPIKvW59WQf1tJNx/ZZKPfN+EhPX3Z6CYY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0/go.mod h1:TpiwjwnW/khS0LKs4vW5UmmT9OWcxaveS8U7+tlknzo= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= -github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY= -github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= -github.com/GeertJohan/go.rice v1.0.2/go.mod h1:af5vUNlDNkCjOZeSGFgIJxDje9qdjsO6hshx0gTmZt4= -github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae/go.mod h1:mjwGPas4yKduTyubHvD1Atl9r1rUq8DfVy+gkVvZ+oo= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= -github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3 h1:mw6pDQqv38/WGF1cO/jF5t/jyAJ2yi7CmtFLLO5tGFI= -github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= -github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= -github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= -github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= -github.com/Microsoft/hcsshim v0.8.16 h1:8/auA4LFIZFTGrqfKhGBSXwM6/4X1fHa/xniyEHu8ac= -github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= -github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= -github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/hcsshim v0.9.4 h1:mnUj0ivWy6UzbB1uLFqKR6F+ZyiDc7j4iGgHTpO+5+I= +github.com/Microsoft/hcsshim v0.9.4/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= github.com/Rican7/retry v0.3.0/go.mod h1:CxSDrhAyXmTMeEuRAnArMu1FHu48vtfjLREWqVl7Vw0= github.com/Rican7/retry v0.3.1 h1:scY4IbO8swckzoA/11HgBwaZRJEyY9vaNJshcdhp1Mc= github.com/Rican7/retry v0.3.1/go.mod h1:CxSDrhAyXmTMeEuRAnArMu1FHu48vtfjLREWqVl7Vw0= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/adrg/xdg v0.3.3 h1:s/tV7MdqQnzB1nKY8aqHvAMD+uCiuEDzVB5HLRY849U= github.com/adrg/xdg v0.3.3/go.mod h1:61xAR2VZcggl2St4O9ohF5qCKe08+JDmE4VNzPFQvOQ= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= -github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= -github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antonlindstrom/pgstore v0.0.0-20220421113606-e3a6e3fed12a h1:dIdcLbck6W67B5JFMewU5Dba1yKZA3MsT67i4No/zh0= github.com/antonlindstrom/pgstore v0.0.0-20220421113606-e3a6e3fed12a/go.mod h1:Sdr/tmSOLEnncCuXS5TwZRxuk7deH1WXVY8cve3eVBM= -github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= -github.com/apache/beam v2.28.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ= -github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= -github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= -github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs= -github.com/armon/go-metrics v0.3.3/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/armon/go-metrics v0.4.0 h1:yCQqn7dwca4ITXb+CbubHmedzaQYHhNhrEXLYUeEe8Q= -github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= -github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.19.45/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.25.11/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/aws/aws-sdk-go-v2 v1.20.1/go.mod h1:NU06lETsFm8fUC6ZjhgDpVBcGZTFQ6XM+LZWZxMI4ac= -github.com/aws/aws-sdk-go-v2 v1.20.2/go.mod h1:NU06lETsFm8fUC6ZjhgDpVBcGZTFQ6XM+LZWZxMI4ac= -github.com/aws/aws-sdk-go-v2 v1.20.3/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M= -github.com/aws/aws-sdk-go-v2 v1.21.0 h1:gMT0IW+03wtYJhRqTVYn0wLzwdnK9sRMcxmtfGzRdJc= -github.com/aws/aws-sdk-go-v2 v1.21.0/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno= -github.com/aws/aws-sdk-go-v2/config v1.18.35 h1:uU9rgCzrW/pVRUUlRULiwKQe8RoEDst1NQu4Qo8kOtk= -github.com/aws/aws-sdk-go-v2/config v1.18.35/go.mod h1:7xF1yr9GBMfYRQI4PLHO8iceqKLM6DpGVEvXI38HB/A= -github.com/aws/aws-sdk-go-v2/credentials v1.13.34/go.mod h1:+wgdxCGNulHme6kTMZuDL9KOagLPloemoYkfjpQkSEU= -github.com/aws/aws-sdk-go-v2/credentials v1.13.35 h1:QpsNitYJu0GgvMBLUIYu9H4yryA5kMksjeIVQfgXrt8= -github.com/aws/aws-sdk-go-v2/credentials v1.13.35/go.mod h1:o7rCaLtvK0hUggAGclf76mNGGkaG5a9KWlp+d9IpcV8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.10/go.mod h1:wMsSLVM2hRpDVhd+3dtLUzqwm7/fjuhNN+b1aOLDt6g= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 h1:uDZJF1hu0EVT/4bogChk8DyjSF6fof6uL/0Y26Ma7Fg= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11/go.mod h1:TEPP4tENqBGO99KwVpV9MlOX4NSrSLP8u3KRy2CDwA8= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.38/go.mod h1:qggunOChCMu9ZF/UkAfhTz25+U2rLVb3ya0Ua6TTfCA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.39/go.mod h1:OLmjwglQh90dCcFJDGD+T44G0ToLH+696kRwRhS1KOU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.40/go.mod h1:5kKmFhLeOVy6pwPDpDNA6/hK/d6URC98pqDDqHgdBx4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 h1:22dGT7PneFMx4+b3pz7lMTRyN8ZKH7M2cW4GP9yUS2g= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41/go.mod h1:CrObHAuPneJBlfEJ5T3szXOUkLEThaGfvnhTf33buas= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.32/go.mod h1:0ZXSqrty4FtQ7p8TEuRde/SZm9X05KT18LAUlR40Ln0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.33/go.mod h1:S/zgOphghZAIvrbtvsVycoOncfqh1Hc4uGDIHqDLwTU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.34/go.mod h1:RZP0scceAyhMIQ9JvFp7HvkpcgqjL4l/4C+7RAeGbuM= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 h1:SijA0mgjV8E+8G45ltVHs0fvKpTj8xmZJ3VwhGKtUSI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35/go.mod h1:SJC1nEVVva1g3pHAIdCp7QsRIkMmLAgoDquQ9Rr8kYw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.41/go.mod h1:mKxUXW+TuwpCKKHVlmHGVVuBi9y9LKW8AiQodg23M5E= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 h1:GPUcE/Yq7Ur8YSUk6lVkoIMWnJNO0HT18GUzCWCgCI0= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42/go.mod h1:rzfdUlfA+jdgLDmPKjd3Chq9V7LVLYo1Nz++Wb91aRo= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.20 h1:YIvKIfPXQVp0EhXUV644kmQo6cQPPSRmC44A1HSoJeg= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.20/go.mod h1:8W88sW3PjamQpKFUQvHWWKay6ARsNvZnzU7+a4apubw= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.113.0 h1:r6pW/VOm8ea4GDEmwDwN2IkgYmu8JjcYzYvHJRs5sEw= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.113.0/go.mod h1:UAWT8Tspir6mGp9WKvKWALaMkPgX1gnkSYZb5oo18XI= -github.com/aws/aws-sdk-go-v2/service/ecr v1.19.2 h1:w0gKerNa4omzguFtH0bkX+lXjUvwoXNdBcmWvFwd7E4= -github.com/aws/aws-sdk-go-v2/service/ecr v1.19.2/go.mod h1:jcU1u1nvnJhPCqNk9ZOJmFEkKJsbRw5oYEYHH4sfOAQ= -github.com/aws/aws-sdk-go-v2/service/iam v1.22.2 h1:DPFxx/6Zwes/MiadlDteVqDKov7yQ5v9vuwfhZuJm1s= -github.com/aws/aws-sdk-go-v2/service/iam v1.22.2/go.mod h1:cQTMNdo/Z5t1DDRsUnx0a2j6cPnytMBidUYZw2zks28= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.23 h1:c5+bNdV8E4fIPteWx4HZSkqI07oY9exbfQ7JH7Yx4PI= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.23/go.mod h1:1jcUfF+FAOEwtIcNiHPaV4TSoZqkUIPzrohmD7fb95c= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.33/go.mod h1:kcNtzCcEoflp+6e2CDTmm2h3xQGZOBZqYA/8DhYx/S8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.34/go.mod h1:ytsF+t+FApY2lFnN51fJKPhH6ICKOPXKEcwwgmJEdWI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 h1:CdzPW9kKitgIiLV1+MHobfR5Xg25iYnyzWZhyQuSlDI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35/go.mod h1:QGF2Rs33W5MaN9gYdEQOBBFPLwTZkEhRwI33f7KIG0o= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.22 h1:ISLJ2BKXe4zzyZ7mp5ewKECiw0U7KpLgS3S6OxY9Cm0= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.22/go.mod h1:QFVbqK54XArazLvn2wvWMRBi/jGrWii46qbr5DyPGjc= -github.com/aws/aws-sdk-go-v2/service/s3 v1.30.3 h1:PVieHTwugdlHedlxLpYLQsOZAq736RScuEb/m4zhzc4= -github.com/aws/aws-sdk-go-v2/service/s3 v1.30.3/go.mod h1:XN3YcdmnWYZ3Hrnojvo5p2mc/wfF973nkq3ClXPDMHk= -github.com/aws/aws-sdk-go-v2/service/sso v1.13.4/go.mod h1:FP05hDXTLouXwAMQ1swqybHy7tHySblMkBMKSumaKg0= -github.com/aws/aws-sdk-go-v2/service/sso v1.13.5 h1:oCvTFSDi67AX0pOX3PuPdGFewvLRU2zzFSrTsgURNo0= -github.com/aws/aws-sdk-go-v2/service/sso v1.13.5/go.mod h1:fIAwKQKBFu90pBxx07BFOMJLpRUGu8VOzLJakeY+0K4= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.4/go.mod h1:4pdlNASc29u0j9bq2jIQcBghG5Lx2oQAIj91vo1u1t8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5 h1:dnInJb4S0oy8aQuri1mV6ipLlnZPfnsDNB9BGO9PDNY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5/go.mod h1:yygr8ACQRY2PrEcy3xsUI357stq2AxnFM6DIsR9lij4= -github.com/aws/aws-sdk-go-v2/service/sts v1.21.4/go.mod h1:CQRMCzYvl5eeAQW3AWkRLS+zGGXCucBnsiQlrs+tCeo= -github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 h1:CQBFElb0LS8RojMJlxRSo/HXipvTZW2S44Lt9Mk2aYQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.21.5/go.mod h1:VC7JDqsqiwXukYEDjoHh9U0fOJtNWh04FPQz4ct4GGU= -github.com/aws/smithy-go v1.14.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= -github.com/aws/smithy-go v1.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ= -github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= -github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= +github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= +github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2/config v1.26.2 h1:+RWLEIWQIGgrz2pBPAUoGgNGs1TOyF4Hml7hCnYj2jc= +github.com/aws/aws-sdk-go-v2/config v1.26.2/go.mod h1:l6xqvUxt0Oj7PI/SUXYLNyZ9T/yBPn3YTQcJLLOdtR8= +github.com/aws/aws-sdk-go-v2/credentials v1.16.13 h1:WLABQ4Cp4vXtXfOWOS3MEZKr6AAYUpMczLhgKtAjQ/8= +github.com/aws/aws-sdk-go-v2/credentials v1.16.13/go.mod h1:Qg6x82FXwW0sJHzYruxGiuApNo31UEtJvXVSZAXeWiw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.142.0 h1:VrFC1uEZjX4ghkm/et8ATVGb1mT75Iv8aPKPjUE+F8A= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.142.0/go.mod h1:qjhtI9zjpUHRc6khtrIM9fb48+ii6+UikL3/b+MKYn0= +github.com/aws/aws-sdk-go-v2/service/ecr v1.24.6 h1:cT7h+GWP2k0hJSsPmppKgxl4C9R6gCC5/oF4oHnmpK4= +github.com/aws/aws-sdk-go-v2/service/ecr v1.24.6/go.mod h1:AOHmGMoPtSY9Zm2zBuwUJQBisIvYAZeA1n7b6f4e880= +github.com/aws/aws-sdk-go-v2/service/iam v1.28.6 h1:P5oJkH50fc9mKjrzEMtYYCdMBhrbVPQsvlsD3L56Itg= +github.com/aws/aws-sdk-go-v2/service/iam v1.28.6/go.mod h1:kKI0gdVsf+Ev9knh/3lBJbchtX5LLNH25lAzx3KDj3Q= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.6 h1:HJeiuZ2fldpd0WqngyMR6KW7ofkXNLyOaHwEIGm39Cs= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.6/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= +github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= +github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= -github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bmizerany/pat v0.0.0-20160217103242-c068ca2f0aac h1:X5YRFJiteUM3rajABEYJSzw1KWgmp1ulPFKxpfLm0M4= -github.com/bmizerany/pat v0.0.0-20160217103242-c068ca2f0aac/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= -github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw= -github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo= -github.com/canonical/candid v1.12.2 h1:0hfZZ1qBpFCoCrirSeSdxmOWvQnCs+1cjpft9yRs0so= -github.com/canonical/candid v1.12.2/go.mod h1:NiCD+Go6m2oxWcsfntU9t2Cs6uZbs8UaTRt3ySubaXU= -github.com/canonical/go-dqlite v1.20.0 h1:pnkn0oS0hPXWeODjvjWONKGb5KYh8kK0aruDPzZLwmU= -github.com/canonical/go-dqlite v1.20.0/go.mod h1:Uvy943N8R4CFUAs59A1NVaziWY9nJ686lScY7ywurfg= +github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f h1:gOO/tNZMjjvTKZWpY7YnXC72ULNLErRtp94LountVE8= +github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= +github.com/canonical/go-dqlite v1.21.0 h1:4gLDdV2GF+vg0yv9Ff+mfZZNQ1JGhnQ3GnS2GeZPHfA= +github.com/canonical/go-dqlite v1.21.0/go.mod h1:Uvy943N8R4CFUAs59A1NVaziWY9nJ686lScY7ywurfg= +github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8 h1:zGaJEJI9qPVyM+QKFJagiyrM91Ke5S9htoL1D470g6E= +github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8/go.mod h1:ZZFeR9K9iGgpwOaLYF9PdT44/+lfSJ9sQz3B+SsGsYU= github.com/canonical/go-service v1.0.0 h1:TF6TsEp04xAoI5pPoWjTYmEwLjbPATSnHEyeJCvzElg= github.com/canonical/go-service v1.0.0/go.mod h1:GzNLXpkGdglL0kjREXoLXj2rB2Qx+EvAGncRDqCENYQ= -github.com/canonical/lxd v0.0.0-20230712132802-8d2a42545fd0 h1:1JfA4hOWjPoF18ebpKFWafOWFplCh0jvHhAethmLQFo= -github.com/canonical/lxd v0.0.0-20230712132802-8d2a42545fd0/go.mod h1:BAaklWDYuotKE0eQnwO6NArKc6rEwnTheuOPrtlLBYA= +github.com/canonical/lxd v0.0.0-20231214113525-e676fc63c50a h1:Tfo/MzXK5GeG7gzSHqxGeY/669Mhh5ea43dn1mRDnk8= +github.com/canonical/lxd v0.0.0-20231214113525-e676fc63c50a/go.mod h1:UxfHGKFoRjgu1NUA9EFiR++dKvyAiT0h9HT0ffMlzjc= github.com/canonical/ofga v0.10.0 h1:DHXhG/DAXWWQT/I+2jzr4qm0uTIYrILmtMxd6ZqmEzE= github.com/canonical/ofga v0.10.0/go.mod h1:u4Ou8dbIhO7FmVlT7W3rX2roD9AOGz/CqmGh7AdF0Lo= -github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= -github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/canonical/pebble v1.9.0 h1:FWVEh1fg3aaW2HNue2Z2eYMwkJEQT8mgMFW3R5Iocn4= +github.com/canonical/pebble v1.9.0/go.mod h1:9Qkjmq298g0+9SvM2E5eekkEN4pjHDWhgg9eB2I0tjk= +github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b h1:Da2fardddn+JDlVEYtrzBLTtyzoyU3nIS0Cf0GvjmwU= +github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b/go.mod h1:upTK9n6rlqITN9rCN69hdreI37dRDFUk2thlGGD5Cg8= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= -github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= -github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= -github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s= -github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= -github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= -github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/cjlapao/common-go v0.0.39 h1:bAAUrj2B9v0kMzbAOhzjSmiyDy+rd56r2sy7oEiQLlA= github.com/cjlapao/common-go v0.0.39/go.mod h1:M3dzazLjTjEtZJbbxoA5ZDiGCiHmpwqW9l4UWaddwOA= -github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY= -github.com/cloudflare/cfssl v1.6.1 h1:aIOUjpeuDJOpWjVJFP2ByplF53OgqG8I1S40Ggdlk3g= -github.com/cloudflare/cfssl v1.6.1/go.mod h1:ENhCj4Z17+bY2XikpxVmTHDg/C2IsG2Q0ZBeXpAqhCk= -github.com/cloudflare/redoctober v0.0.0-20201013214028-99c99a8e7544/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210322005330-6414d713912e/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5 h1:xD/lrqdvwsc+O2bjSSi3YqY73Ke3LAiSCx49aCesA0E= -github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= -github.com/cockroachdb/errors v1.2.4 h1:Lap807SXTH5tri2TivECb/4abUkMZC9zRoLarvcKDqs= -github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= -github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY= -github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= -github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= -github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= -github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= -github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= -github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= -github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= -github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68 h1:hkGVFjz+plgr5UfxZUTPFbUFIF/Km6/s+RVRIRHLrrY= -github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= -github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= -github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= -github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= -github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= -github.com/containerd/containerd v1.5.0-beta.4 h1:zjz4MOAOFgdBlwid2nNUlJ3YLpVi/97L36lfMYJex60= -github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= -github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20200709052629-daa8e1ccc0bc/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= -github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= -github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= -github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= -github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= -github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= -github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= -github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= -github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= -github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= -github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= -github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= -github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= -github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= -github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= -github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= -github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= -github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= -github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= +github.com/containerd/cgroups v1.0.4/go.mod h1:nLNQtsF7Sl2HxNebu77i1R0oDlhiTG+kO4JTrUzo6IA= +github.com/containerd/containerd v1.6.8 h1:h4dOFDwzHmqFEP754PgfgTeVXFnLiRc6kiqC7tplDJs= +github.com/containerd/containerd v1.6.8/go.mod h1:By6p5KqPK0/7/CgO/A6t/Gz+CUYUu2zf1hUaaymVXB0= github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc= github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= -github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= -github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= -github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= -github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= -github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= -github.com/daaku/go.zipexe v1.0.1/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8= -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892/go.mod h1:CTDl0pzVzE5DEzZhPfvhY/9sPFMQIxaJ9VAMs9AagrE= -github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= -github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= -github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= -github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v1.4.2-0.20200319182547-c7ad2b866182/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v20.10.7+incompatible h1:Z6O9Nhsjv+ayUEeI1IojKbYcsGdgYSNqxe1s2MYzUhQ= -github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= +github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/duo-labs/webauthn v0.0.0-20220815211337-00c9fb5711f5 h1:BaeJtFDlto/NjX9t730OebRRJf2P+t9YEDz3ur18824= -github.com/duo-labs/webauthn v0.0.0-20220815211337-00c9fb5711f5/go.mod h1:Jcj7rFNlTknb18v9jpSA58BveX2LDhXqaoy+6YV1N9g= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/dustinkirkland/golang-petname v0.0.0-20230626224747-e794b9370d49 h1:6SNWi8VxQeCSwmLuTbEvJd7xvPmdS//zvMBWweZLgck= -github.com/dustinkirkland/golang-petname v0.0.0-20230626224747-e794b9370d49/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= -github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 h1:S6Dco8FtAhEI/qkg/00H6RdEGC+MCy5GPiQ+xweNRFE= +github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f h1:7T++XKzy4xg7PKy+bM+Sa9/oe1OC88yz2hXQUISoXfA= -github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.1/go.mod h1:txg5va2Qkip90uYoSKH+nkAAmXrb2j3iq4FLwdrCbXQ= -github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8= -github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= -github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 h1:fmFk0Wt3bBxxwZnu48jqMdaOR/IZ4vdtJFuaFV8MpIE= github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3/go.mod h1:bJWSKrZyQvfTnb2OudyUjurSG4/edverV7n82+K3JiM= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= -github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= github.com/frankban/quicktest v1.1.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= -github.com/frankban/quicktest v1.1.1/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= github.com/frankban/quicktest v1.5.0/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= -github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= -github.com/fullstorydev/grpcurl v1.8.0/go.mod h1:Mn2jWbdMrQGJQ8UD62uNyMumT2acsZUCkZIqFxsQf1o= -github.com/fullstorydev/grpcurl v1.8.1 h1:Pp648wlTTg3OKySeqxM5pzh8XF6vLqrm8wRq66+5Xo0= -github.com/fullstorydev/grpcurl v1.8.1/go.mod h1:3BWhvHZwNO7iLXaQlojdg5NA6SxUDePli4ecpK1N7gw= -github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= -github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= -github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04= github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I= github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= -github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= -github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= -github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg= @@ -604,59 +261,35 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-goose/goose/v5 v5.0.0-20230421180421-abaee9096e3a h1:H/l82+fC6idmYg1kfpQlCq7gYctri7AGn9RemqwN6bw= github.com/go-goose/goose/v5 v5.0.0-20230421180421-abaee9096e3a/go.mod h1:BxICmnmP7QlxZhKP2BHkpWQS0tbb3LrsrLtd9TQyyms= -github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8= -github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1 h1:uvQJoKTHrFFu8zxoaopNKedRzwdy3+8H72we4T/5cGs= github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1/go.mod h1:H59IYeChwvD1po3dhGUPvq5na+4NVD7SJlbhGKvslr0= github.com/go-macaroon-bakery/macaroonpb v1.0.0 h1:It9exBaRMZ9iix1iJ6gwzfwsDE6ExNuwtAJ9e09v6XE= github.com/go-macaroon-bakery/macaroonpb v1.0.0/go.mod h1:UzrGOcbiwTXISFP2XDLDPjfhMINZa+fX/7A2lMd31zc= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe h1:zn8tqiUbec4wR94o7Qj3LZCAT6uGobhEgnDRg6isG5U= github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= @@ -665,23 +298,11 @@ github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= -github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= -github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -716,18 +337,10 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= -github.com/google/certificate-transparency-go v1.1.2-0.20210422104406-9f33727a7a18/go.mod h1:6CKh9dscIRoqc2kC6YUFICHZMT9NrClyPrRVFrdw1QQ= -github.com/google/certificate-transparency-go v1.1.2-0.20210511102531-373a877eec92 h1:806qveZBQtRNHroYHyg6yrsjqBJh9kIB4nfmB8uJnak= -github.com/google/certificate-transparency-go v1.1.2-0.20210511102531-373a877eec92/go.mod h1:kXWPsHVPSKVuxPPG69BRtumCbAW537FydV/GH89oBhM= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -741,23 +354,15 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= -github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOmkVoJOpwnS0wfdsJCV9CoD5nJYsHoFk/0CrTK4M= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= -github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/licenseclassifier v0.0.0-20210325184830-bb04aff29e72/go.mod h1:qsqn2hxC+vURpyBRygGUuinTO42MFRLcsmQ/P8v94+M= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -769,124 +374,61 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU= github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= -github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg= -github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= -github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= -github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= -github.com/google/trillian v1.3.14-0.20210409160123-c5ea3abd4a41/go.mod h1:1dPv0CUjNQVFEDuAUFhZql16pw/VlPgaX8qj+g5pVzQ= -github.com/google/trillian v1.3.14-0.20210428093031-b4ddea2e86b1/go.mod h1:FdIJX+NoDk/dIN2ZxTyz5nAJWgf+NSSSriPAMThChTY= -github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v0.0.0-20170306145142-6a5e28554805/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s= -github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= -github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= -github.com/goreleaser/goreleaser v0.134.0/go.mod h1:ZT6Y2rSYa6NxQzIsdfWWNWAlYGXGbreo66NmE+3X3WQ= -github.com/goreleaser/nfpm v1.2.1/go.mod h1:TtWrABZozuLOttX2uDlYyECfQX7x5XYkVxhjYcR6G9w= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= -github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/schema v1.2.1 h1:tjDxcmdb+siIqkTNoV+qRH2mjYdr2hHe5MKXbp61ziM= +github.com/gorilla/schema v1.2.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.2/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4GkDEdKCRJduHpTxT3/jcw= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.16.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= -github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= -github.com/hashicorp/go-plugin v1.4.5 h1:oTE/oQR4eghggRg8VY7PAz3dr++VwDNBGCcOfIvHpBo= -github.com/hashicorp/go-plugin v1.4.5/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.6.2/go.mod h1:gEx6HMUGxYYhJScX7W1Il64m6cc2C1mDaW3NQ9sY1FY= -github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= -github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= @@ -898,18 +440,10 @@ github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjG github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= -github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -918,42 +452,19 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f/go.mod h1:euTFbi2YJgwcju3imEt919lhJKF68nN1cQPq3aA+kBE= -github.com/hashicorp/vault/api v1.1.1/go.mod h1:29UXcn/1cLOPHQNMWA7bCz2By4PSd0VKPAydKXS5yN0= -github.com/hashicorp/vault/api v1.8.2 h1:C7OL9YtOtwQbTKI9ogB0A1wffRbCN+rH/LLCHO3d8HM= -github.com/hashicorp/vault/api v1.8.2/go.mod h1:ML8aYzBIhY5m1MD1B2Q0JV89cC85YVH4t5kBaZiyVaE= -github.com/hashicorp/vault/sdk v0.1.14-0.20200519221530-14615acda45f/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10= -github.com/hashicorp/vault/sdk v0.2.1/go.mod h1:WfUiO1vYzfBkz1TmoE4ZGU7HD0T0Cl/rZwaxjBkgN4U= -github.com/hashicorp/vault/sdk v0.6.0 h1:6Z+In5DXHiUfZvIZdMx7e2loL1PPyDjA4bVh9ZTIAhs= -github.com/hashicorp/vault/sdk v0.6.0/go.mod h1:+DRpzoXIdMvKc88R4qxr+edwy/RvH5QK8itmxLiDHLc= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= -github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= -github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= -github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/hashicorp/vault/api v1.10.0 h1:/US7sIjWN6Imp4o/Rj1Ce2Nr5bki/AXi9vAW3p2tOJQ= +github.com/hashicorp/vault/api v1.10.0/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/im7mortal/kmutex v1.0.1 h1:zAACzjwD+OEknDqnLdvRa/BhzFM872EBwKijviGLc9Q= github.com/im7mortal/kmutex v1.0.1/go.mod h1:f71c/Ugk/+58OHRAgvgzPP3QEiWGUjK13fd8ozfKWdo= -github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/itchyny/gojq v0.12.12 h1:x+xGI9BXqKoJQZkr95ibpe3cdrTbY8D9lonrK433rcA= github.com/itchyny/gojq v0.12.12/go.mod h1:j+3sVkjxwd7A7Z5jrbKibgOLn0ZfLWkV+Awxr/pyzJE= github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= -github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= @@ -1020,10 +531,8 @@ github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv github.com/jackc/puddle v1.1.2/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= @@ -1032,44 +541,23 @@ github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aW github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSnpuG7toUTG4= -github.com/jhump/protoreflect v1.8.2 h1:k2xE7wcUomeqwY0LDCYA16y4WWfyTcMx5mKhk0d4ua0= -github.com/jhump/protoreflect v1.8.2/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= -github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= -github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/juju/aclstore/v2 v2.1.0 h1:aIZmJw4cLAKLKlvU0yaM6b2HBSRgFjILJ2pqtZnp7aU= -github.com/juju/aclstore/v2 v2.1.0/go.mod h1:c2AibPW3K+JKHmkqlvPET9hfbe2vPa3e1GuNw76NlKM= github.com/juju/ansiterm v0.0.0-20160907234532-b99631de12cf/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/juju/ansiterm v0.0.0-20210706145210-9283cdf370b5/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= @@ -1077,9 +565,8 @@ github.com/juju/ansiterm v1.0.0 h1:gmMvnZRq7JZJx6jkfSq9/+2LMrVEwGwt7UR6G+lmDEg= github.com/juju/ansiterm v1.0.0/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384= github.com/juju/blobstore/v3 v3.0.2 h1:roZ4YBuZYmWId6y/6ZLQSAMmNlHOclHD8PQAMOQer6E= github.com/juju/blobstore/v3 v3.0.2/go.mod h1:NXEgMhrVH5744/zLfSkzsySlDQUpCgzvcNxjJJhICko= -github.com/juju/charm/v11 v11.0.2 h1:qVcrG9X5fTN++aBfM4QwzOQRd6h31degr3vD2fTgjxs= -github.com/juju/charm/v11 v11.0.2/go.mod h1:Mge5Ko3pPgocmk4v1pQgmBhF8BuBLGTCFu3jq83JvHk= -github.com/juju/clock v0.0.0-20180808021310-bab88fc67299/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= +github.com/juju/charm/v12 v12.0.0 h1:/h3YRMqbgxT89QkQGgMS/myOxuHy/kzBLCDOvodsoFY= +github.com/juju/charm/v12 v12.0.0/go.mod h1:rX3no84EHT+qN+BGtwqPyvueC1Sxr0bXWxsbUd6i1iY= github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= github.com/juju/clock v0.0.0-20220202072423-1b0f830854c4/go.mod h1:zDZCPSgCJQINeZtQwHx2/cFk4seaBC8Yiqe8V82xiP0= github.com/juju/clock v0.0.0-20220203021603-d9deb868a28a/go.mod h1:GZ/FY8Cqw3KHG6DwRVPUKbSPTAwyrU28xFi5cqZnLsc= @@ -1095,11 +582,9 @@ github.com/juju/collections v0.0.0-20220203020748-febd7cad8a7a/go.mod h1:JWeZdyt github.com/juju/collections v1.0.0/go.mod h1:JWeZdyttIEbkR51z2S13+J+aCuHVe0F6meRy+P0YGDo= github.com/juju/collections v1.0.4 h1:GjL+aN512m2rVDqhPII7P6qB0e+iYFubz8sqBhZaZtk= github.com/juju/collections v1.0.4/go.mod h1:hVrdB0Zwq9wIU1Fl6ItD2+UETeNeOEs+nGvJufVe+0c= -github.com/juju/description/v4 v4.0.11 h1:eNF/PUJf5FeBRQe0XVPC4RWTnxb7kTNodLcCVea7Ebw= -github.com/juju/description/v4 v4.0.11/go.mod h1:LRv+oC6zWwK+MpIEC3TCzRXjw5d75WK1HjcvNTWP+e8= +github.com/juju/description/v5 v5.0.0 h1:koySpaGHVTvoHlr+siRLxVKS/Jzilud5iGzjE7tldks= +github.com/juju/description/v5 v5.0.0/go.mod h1:TQsp2Z56EVab7onQY2/O6tLHznovAcckyLz/DgDfVPY= github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= -github.com/juju/errors v0.0.0-20180726005433-812b06ada177/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= -github.com/juju/errors v0.0.0-20190207033735-e65537c515d7/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/errors v0.0.0-20200330140219-3fe23663418f/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/errors v0.0.0-20210818161939-5560c4c073ff/go.mod h1:i1eL7XREII6aHpQ2gApI/v6FkVUDEBremNkcBCKYAcY= github.com/juju/errors v0.0.0-20220203013757-bd733f3c86b9/go.mod h1:TRm7EVGA3mQOqSVcBySRY7a9Y1/gyVhh/WTCnc5sD4U= @@ -1122,16 +607,14 @@ github.com/juju/gomaasapi/v2 v2.2.0 h1:vaYeEKr0mQXsM38/zfWqCrYDG8cYhHRkfTQMgkJGH github.com/juju/gomaasapi/v2 v2.2.0/go.mod h1:ZsohFbU4xShV1aSQYQ21hR1lKj7naNGY0SPuyelcUmk= github.com/juju/http/v2 v2.0.0 h1:xexT4KO4jY0UEkhLZPwQGaEGCfgWWjw3jBPrLRumhKI= github.com/juju/http/v2 v2.0.0/go.mod h1:hqjfdSwUaD23PurU7FZyY/rsqVL90RF3w4Xl0iPILRA= -github.com/juju/httpprof v0.0.0-20141217160036-14bf14c30767 h1:COsaGcfAONDdIDnGS8yFdxOyReP7zKQEr7jFzCHKDkM= github.com/juju/httpprof v0.0.0-20141217160036-14bf14c30767/go.mod h1:+MaLYz4PumRkkyHYeXJ2G5g5cIW0sli2bOfpmbaMV/g= github.com/juju/idmclient/v2 v2.0.0 h1:PsGa092JGy6iFNHZCcao+bigVsTyz1C+tHNRdYmKvuE= github.com/juju/idmclient/v2 v2.0.0/go.mod h1:EOiFbPmnkqKvCUS/DHpDRWhL7eKF0AJaTvMjIYlIUak= github.com/juju/jsonschema v1.0.0 h1:2ScR9hhVdHxft+Te3fnclVx61MmlikHNEOirTGi+hV4= github.com/juju/jsonschema v1.0.0/go.mod h1:SlFW+jFtpWX0P4Tb+zTTPR4ufttLrnJIdQPePxVEfkM= -github.com/juju/juju v0.0.0-20231110092546-f835120f6974 h1:0R8UNV60nhQZGCiDw+vyxUNyQCLEUEUsRXn/o0mG3RM= -github.com/juju/juju v0.0.0-20231110092546-f835120f6974/go.mod h1:p69SgkS5SoBGeF+Bg60e2kcaC7ImVvmyrmXlzDsLb3s= +github.com/juju/juju v0.0.0-20240304110523-55fb5d03683b h1:rowVYnJXJQup9dFBNLx3PCa9NWZ63jgewlwHB2HDBEk= +github.com/juju/juju v0.0.0-20240304110523-55fb5d03683b/go.mod h1:lG1192+QZsfFbVI+3Vg9KDv+F2tGc+8marXD9E1Vnuc= github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= -github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20190212223446-d976af380377/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20200526014432-9ce3a2e09b5e/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4/go.mod h1:NIXFioti1SmKAlKNuUwbMenNdef59IF52+ZzuOmHYkg= @@ -1150,9 +633,6 @@ github.com/juju/mgo/v3 v3.0.4/go.mod h1:fAvhDCRbUlEbRIae6UQT8RvPUoLwKnJsBgO6OzHK github.com/juju/mgomonitor v0.0.0-20181029151116-52206bb0cd31 h1:v6GpXmpXOD6KwPbApRlwDGQxf1FpS6gfLdfVbE4ZLzk= github.com/juju/mgomonitor v0.0.0-20181029151116-52206bb0cd31/go.mod h1:m6E+J+I+cE+6rcaVxSI4HwGLIEOCSOBMYedt3Sewh+U= github.com/juju/mgotest v1.0.1/go.mod h1:vTaDufYul+Ps8D7bgseHjq87X8eu0ivlKLp9mVc/Bfc= -github.com/juju/mgotest v1.0.2/go.mod h1:04v1Xi2RiTO3h77YWtaXB2LAaGRSSi+Vl4hOV1coD0k= -github.com/juju/mgotest v1.0.3 h1:3UIS2cOSzE6qz/dtiLAaQew5AKYw/bRb++/lsB522HI= -github.com/juju/mgotest v1.0.3/go.mod h1:Dnzi6seljG9GoZpqFdTqRV3ybB3UcIj+H8iQqy1so1A= github.com/juju/mutex v0.0.0-20171110020013-1fe2a4bf0a3a/go.mod h1:Y3oOzHH8CQ0Ppt0oCKJ2JFO81/EsWenH5AEqigLH+yY= github.com/juju/mutex/v2 v2.0.0-20220128011612-57176ebdcfa3/go.mod h1:TTCG9BJD9rCC4DZFz3jA0QvCqFDHw8Eqz0jstwY7RTQ= github.com/juju/mutex/v2 v2.0.0-20220203023141-11eeddb42c6c/go.mod h1:jwCfBs/smYDaeZLqeaCi8CB8M+tOes4yf827HoOEoqk= @@ -1160,6 +640,8 @@ github.com/juju/mutex/v2 v2.0.0 h1:rVmJdOaXGWF8rjcFHBNd4x57/1tks5CgXHx55O55SB0= github.com/juju/mutex/v2 v2.0.0/go.mod h1:jwCfBs/smYDaeZLqeaCi8CB8M+tOes4yf827HoOEoqk= github.com/juju/names/v4 v4.0.0 h1:XeQZbwT70i98TynM+2RJr9At6EGb9X/P6l8qF56hPns= github.com/juju/names/v4 v4.0.0/go.mod h1:xpkrQpHbz1DGY+0Geo32ZnyognGA/2vSB++rpu/Z+Lc= +github.com/juju/names/v5 v5.0.0 h1:3IkRTUaniNXsgjy4lNqbJx7dVdsONlzuH6YMYT7uXss= +github.com/juju/names/v5 v5.0.0/go.mod h1:PkvHbErUTniKvLu1ejJ5m/AbXOW55MFn1jsGVEbVXk8= github.com/juju/naturalsort v1.0.0 h1:kGmUUy3h8mJ5/SJYaqKOBR3f3owEd5R52Lh+Tjg/dNM= github.com/juju/naturalsort v1.0.0/go.mod h1:Zqa/vGkXr78k47zM6tFmU9phhxKz/PIdqBzpLhJ86zc= github.com/juju/os/v2 v2.2.3 h1:5SnGWfzFTXcFwu/sd9qEEf/No3UZxivOjJuWmsHI4N4= @@ -1177,13 +659,11 @@ github.com/juju/qthttptest v0.0.1/go.mod h1://LCf/Ls22/rPw2u1yWukUJvYtfPY4nYpWUl github.com/juju/qthttptest v0.1.1/go.mod h1:aTlAv8TYaflIiTDIQYzxnl1QdPjAg8Q8qJMErpKy6A4= github.com/juju/qthttptest v0.1.3 h1:M0HdpwsK/UTHRGRcIw5zvh5z+QOgdqyK+ecDMN+swwM= github.com/juju/qthttptest v0.1.3/go.mod h1:2gayREyVSs/IovPmwYAtU+HZzuhDjytJQRRLzPTtDYE= -github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/juju/replicaset/v3 v3.0.1 h1:+w2z277yeiRTI1JQ+8sNgKIraUT1uQb+X9WmmlOuT9M= github.com/juju/replicaset/v3 v3.0.1/go.mod h1:2V11mLgGEaTOcQ4XBw6DbZn9mO6u5w7da1CflXCZ8gE= github.com/juju/retry v0.0.0-20151029024821-62c620325291/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= -github.com/juju/retry v0.0.0-20160928201858-1998d01ba1c3/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= github.com/juju/retry v1.0.0 h1:Tb1hFdDSPGLH/BGdYQOF7utQ9lA0ouVJX2imqgJK6tk= github.com/juju/retry v1.0.0/go.mod h1:SssN1eYeK3A2qjnFGTiVMbdzGJ2BfluaJblJXvuvgqA= @@ -1194,13 +674,10 @@ github.com/juju/romulus v1.0.0/go.mod h1:NTUb3tUldFPZLWbieglCiV4SpccHuiFUNRY+bsI github.com/juju/rpcreflect v1.2.0 h1:eokqqHg13bji8S3IRTuN7f5rfUkn+KnLiItoNfcSLoY= github.com/juju/rpcreflect v1.2.0/go.mod h1:yWSyMkWOwQGqUVxvboHn1c3CuCQrQ5LgV//8c/K1DqE= github.com/juju/schema v1.0.0/go.mod h1:Y+ThzXpUJ0E7NYYocAbuvJ7vTivXfrof/IfRPq/0abI= -github.com/juju/schema v1.0.1 h1:GBEiwxZQeoQuXI6gOTG58W/ZpdongMwl9pfZq1KcNgM= -github.com/juju/schema v1.0.1/go.mod h1:Y+ThzXpUJ0E7NYYocAbuvJ7vTivXfrof/IfRPq/0abI= -github.com/juju/simplekv v1.1.0 h1:3j2a817FVp1uwwc7Y0+f9Bok2HSBoyLPJKOAzlQ/z0o= -github.com/juju/simplekv v1.1.0/go.mod h1:OZjCrSxeKfEpNNp3JtM4B8NOVR4EJTffgvRY1qpPZ+w= +github.com/juju/schema v1.2.0 h1:+XywM0pYzuhGebQiK1aR4Bj7Q7nLV5MihcOgq6dLLxs= +github.com/juju/schema v1.2.0/go.mod h1:VdljuJLc45loM79LYm4yKKmPJwK1bPKRekvMVlfywU0= github.com/juju/testing v0.0.0-20180402130637-44801989f0f7/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/juju/testing v0.0.0-20180517134105-72703b1e95eb/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= -github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/juju/testing v0.0.0-20210302031854-2c7ee8570c07/go.mod h1:7lxZW0B50+xdGFkvhAb8bwAGt6IU87JB1H9w4t8MNVM= github.com/juju/testing v0.0.0-20210324180055-18c50b0c2098/go.mod h1:7lxZW0B50+xdGFkvhAb8bwAGt6IU87JB1H9w4t8MNVM= @@ -1214,11 +691,7 @@ github.com/juju/txn/v3 v3.0.2/go.mod h1:DlFlqNZkgzE4NolIxhSvYOok/heIOjhXLx3Z5oUT github.com/juju/usso v1.0.1 h1:zyQhSUJnhFZdPqVAmPeqXYlnYXv+i0Cp1Ii+aziMXGs= github.com/juju/usso v1.0.1/go.mod h1:3cvBcGVmWXyHhrBHBQtpNBzca/JRg4S5XH88Hj/NsYA= github.com/juju/utils v0.0.0-20180424094159-2000ea4ff043/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= -github.com/juju/utils v0.0.0-20180619112806-c746c6e86f4f/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= -github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= github.com/juju/utils v0.0.0-20200116185830-d40c2fe10647/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= -github.com/juju/utils v0.0.0-20200604140309-9d78121a29e0 h1:4XlJ/Wj/bH3zGa2GU+Us72FgtmL1n3dwjP7LW7+TF/o= -github.com/juju/utils v0.0.0-20200604140309-9d78121a29e0/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= github.com/juju/utils/v2 v2.0.0-20200923005554-4646bfea2ef1/go.mod h1:fdlDtQlzundleLLz/ggoYinEt/LmnrpNKcNTABQATNI= github.com/juju/utils/v2 v2.0.0-20210305225158-eedbe7b6b3e2 h1:E7BgV8lczMmMqMtXdOis5BPEDu6bSG1D6K7SHEq7hEw= github.com/juju/utils/v2 v2.0.0-20210305225158-eedbe7b6b3e2/go.mod h1:p35YIk2Pj1lxjhWuYsYbKvMpJ/iX9F8DBgJkNbGF0nQ= @@ -1226,8 +699,8 @@ github.com/juju/utils/v3 v3.0.0-20220130232349-cd7ecef0e94a/go.mod h1:LzwbbEN7bu github.com/juju/utils/v3 v3.0.0-20220202114721-338bb0530e89/go.mod h1:wf5w+8jyTh2IYnSX0sHnMJo4ZPwwuiBWn+xN3DkQg4k= github.com/juju/utils/v3 v3.0.0-20220203023959-c3fbc78a33b0/go.mod h1:8csUcj1VRkfjNIRzBFWzLFCMLwLqsRWvkmhfVAUwbC4= github.com/juju/utils/v3 v3.0.0/go.mod h1:8csUcj1VRkfjNIRzBFWzLFCMLwLqsRWvkmhfVAUwbC4= -github.com/juju/utils/v3 v3.0.2 h1:6Hel0EXKSM4SOQFHfRel74ZvRp4O0QuxSSf3p3W2FNA= -github.com/juju/utils/v3 v3.0.2/go.mod h1:8csUcj1VRkfjNIRzBFWzLFCMLwLqsRWvkmhfVAUwbC4= +github.com/juju/utils/v3 v3.1.0 h1:NrNo73oVtfr7kLP17/BDpubXwa7YEW16+Ult6z9kpHI= +github.com/juju/utils/v3 v3.1.0/go.mod h1:nAj3sHtdYfAkvnkqttTy3Xzm2HzkD9Hfgnc+upOW2Z8= github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= github.com/juju/version v0.0.0-20191219164919-81c1be00b9a6/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= @@ -1242,58 +715,48 @@ github.com/juju/viddy v0.0.0-beta5/go.mod h1:5Yvv+opdIEDt15Tt0++8YvccVykcGJeVXxq github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4/go.mod h1:G6PCelgkM6cuvyD10iYJsjLBsSadVXtJ+nBxFAxE2BU= github.com/juju/webbrowser v1.0.0 h1:JLdmbFtCGY6Qf2jmS6bVaenJFGIFkdF1/BjUm76af78= github.com/juju/webbrowser v1.0.0/go.mod h1:RwVlbBcF91Q4vS+iwlkJ6bZTE3EwlrjbYlM3WMVD6Bc= -github.com/juju/worker/v3 v3.4.0 h1:dMpZJ0RAOFNYKEvKSNA9U/CkWufP/Z3zK4z7QfYzIDI= -github.com/juju/worker/v3 v3.4.0/go.mod h1:ieql+6Kl+Z3akRxgVctilGNF1sc/5Xm2a/inVY0pe4I= +github.com/juju/worker/v3 v3.5.0 h1:pbccjj102IkvdxgCcz0DtQHA5tGZK4PGwm5dUMyRtAc= +github.com/juju/worker/v3 v3.5.0/go.mod h1:ieql+6Kl+Z3akRxgVctilGNF1sc/5Xm2a/inVY0pe4I= github.com/juju/yaml v0.0.0-20200420012109-12a32b78de07 h1:DH1XYlPV0OOzNOOtByWQ38CTT+t3BRzslUHkvQacqaY= github.com/juju/yaml v0.0.0-20200420012109-12a32b78de07/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= github.com/juju/zaputil v0.0.0-20190326175239-ef53049637ac h1:mIYfqlPcFmuFpKMMMmq+pu7okWEWShiyW2w6/+2qDaY= github.com/juju/zaputil v0.0.0-20190326175239-ef53049637ac/go.mod h1:yGXwCw1C3O7X2kkzB5gky65S4I5a0h4Ylic4xVo5D78= -github.com/julienschmidt/httprouter v0.0.0-20151013225520-77a895ad01eb/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.1.1-0.20151013225520-77a895ad01eb/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kian99/juju v0.0.0-20240301094235-2688d7cd925e h1:MnSWbm0Th+V7YI61C4ledtaxg564bzwBdh665fj/MeM= +github.com/kian99/juju v0.0.0-20240301094235-2688d7cd925e/go.mod h1:V5eSJgiG7Evs4ejKhI7na7olYzHR1rxZXwx1/27Sa18= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= -github.com/kisom/goutils v1.4.3/go.mod h1:Lp5qrquG7yhYnWzZCI/68Pa/GpFynw//od6EkGnWpac= -github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/go-gypsy v1.0.0/go.mod h1:chkXM0zjdpXOiqkCW1XcCHDfjfk14PH2KKkQWxfJUcU= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= -github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.0.11 h1:ViHMnaMeaO0qV16RZWBHM7GTrAnX2aFLVKofc7FuKLQ= -github.com/lestrrat-go/jwx/v2 v2.0.11/go.mod h1:ZtPtMFlrfDrH2Y0iwfa3dRFn8VzwBrB+cyrm3IBWdDg= -github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/jwx/v2 v2.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY= +github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat/go-jspointer v0.0.0-20160229021354-f4881e611bdb h1:ZWuRImtpQp2QxwzMFDYqSgym24d7N0HE38JRVoJ/Piw= @@ -1308,34 +771,23 @@ github.com/lestrrat/go-pdebug v0.0.0-20160817063333-2e6eaaa5717f h1:G2QGPA4GVR6H github.com/lestrrat/go-pdebug v0.0.0-20160817063333-2e6eaaa5717f/go.mod h1:VXFH11P7fHn2iPBsfSW1JacR59rttTcafJnwYcI/IdY= github.com/lestrrat/go-structinfo v0.0.0-20160308131105-f74c056fe41f h1:/EgRyJtSpA3GWrYJH7Pl/4/otIAyhP5DSsgi22NVPug= github.com/lestrrat/go-structinfo v0.0.0-20160308131105-f74c056fe41f/go.mod h1:pdNfYLzCLI1dJyJJlwBiSkWRje61vUNlw0NO5cakNRg= -github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= -github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lunixbochs/vtclean v0.0.0-20160125035106-4fbf7632a2c6/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/lyft/protoc-gen-star v0.5.1/go.mod h1:9toiA3cC7z5uVbODF7kEQ91Xn7XNFkVUl+SrEe+ZORU= -github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/masterzen/azure-sdk-for-go v3.2.0-beta.0.20161014135628-ee4f0065d00c+incompatible/go.mod h1:mf8fjOu33zCqxUjuiU3I8S1lJMyEAlH+0F2+M5xl3hE= github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= @@ -1346,103 +798,74 @@ github.com/mattn/go-colorable v0.0.6/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= -github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ= github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= -github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/microsoft/kiota-abstractions-go v1.2.0 h1:lUriJgqdCY/QajwWQOgTCQE9Atywfe2NHhgoTCSXTRE= -github.com/microsoft/kiota-abstractions-go v1.2.0/go.mod h1:RkxyZ5x87Njik7iVeQY9M2wtrrL1MJZcXiI/BxD/82g= -github.com/microsoft/kiota-authentication-azure-go v1.0.0 h1:29FNZZ/4nnCOwFcGWlB/sxPvWz487HA2bXH8jR5k2Rk= -github.com/microsoft/kiota-authentication-azure-go v1.0.0/go.mod h1:rnx3PRlkGdXDcA/0lZQTbBwyYGmc+3POt7HpE/e4jGw= -github.com/microsoft/kiota-http-go v1.0.1 h1:818u3aiLpxj35hZgfUSqphQ18IUTK3gVdTE4cQ5vjLw= -github.com/microsoft/kiota-http-go v1.0.1/go.mod h1:H0cg+ly+5ZSR8z4swj5ea9O/GB5ll2YuYeQ0/pJs7AY= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/microsoft/kiota-abstractions-go v1.5.3 h1:qUTwuXCbMi99EkHaTh5NGMK5MOKxJn7u/M2FbYcesLY= +github.com/microsoft/kiota-abstractions-go v1.5.3/go.mod h1:xyBzTVCYrp7QBW4/p+RFi44PHwp/IPn2dZepuV4nF80= +github.com/microsoft/kiota-authentication-azure-go v1.0.1 h1:F4HH+2QQHSecQg50gVEZaUcxA8/XxCaC2oOMYv2gTIM= +github.com/microsoft/kiota-authentication-azure-go v1.0.1/go.mod h1:IbifJeoi+sULI0vjnsWYSmDu5atFo/4FZ6WCoAkPjsc= +github.com/microsoft/kiota-http-go v1.1.1 h1:W4Olo7Z/MwNZCfkcvH/5eLhnn7koRBMMRhLEnf5MPKo= +github.com/microsoft/kiota-http-go v1.1.1/go.mod h1:QzhhfW5xkoUuT+/ohflpHJvumWeXIxa/Xl0GmQ2M6mY= github.com/microsoft/kiota-serialization-form-go v1.0.0 h1:UNdrkMnLFqUCccQZerKjblsyVgifS11b3WCx+eFEsAI= github.com/microsoft/kiota-serialization-form-go v1.0.0/go.mod h1:h4mQOO6KVTNciMF6azi1J9QB19ujSw3ULKcSNyXXOMA= github.com/microsoft/kiota-serialization-json-go v1.0.4 h1:5TaISWwd2Me8clrK7SqNATo0tv9seOq59y4I5953egQ= github.com/microsoft/kiota-serialization-json-go v1.0.4/go.mod h1:rM4+FsAY+9AEpBsBzkFFis+b/LZLlNKKewuLwK9Q6Mg= +github.com/microsoft/kiota-serialization-multipart-go v1.0.0 h1:3O5sb5Zj+moLBiJympbXNaeV07K0d46IfuEd5v9+pBs= +github.com/microsoft/kiota-serialization-multipart-go v1.0.0/go.mod h1:yauLeBTpANk4L03XD985akNysG24SnRJGaveZf+p4so= github.com/microsoft/kiota-serialization-text-go v1.0.0 h1:XOaRhAXy+g8ZVpcq7x7a0jlETWnWrEum0RhmbYrTFnA= github.com/microsoft/kiota-serialization-text-go v1.0.0/go.mod h1:sM1/C6ecnQ7IquQOGUrUldaO5wj+9+v7G2W3sQ3fy6M= -github.com/microsoftgraph/msgraph-sdk-go v1.14.0 h1:YdhMvzu8bXcfIQGRur6NkXnv4cPOsMBJ44XjfWLOt9Y= -github.com/microsoftgraph/msgraph-sdk-go v1.14.0/go.mod h1:ccLv84FJFtwdSzYWM/HlTes5FLzkzzBsYh9kg93/WS8= -github.com/microsoftgraph/msgraph-sdk-go-core v1.0.0 h1:7NWTfyXvOjoizW7PmxNp3+8wCKPgpODs/D1cUZ3fkAY= -github.com/microsoftgraph/msgraph-sdk-go-core v1.0.0/go.mod h1:tQb4q3YMIj2dWhhXhQSJ4ELpol931ANKzHSYK5kX1qE= +github.com/microsoftgraph/msgraph-sdk-go v1.28.0 h1:WjisgYSVVx0HtHU6EckLog8i0v+i87OpUZgP+wnL2jM= +github.com/microsoftgraph/msgraph-sdk-go v1.28.0/go.mod h1:quVwiVQY6sxPiPR/O0Zli2iqXis1TPQBSEtq/uOcc+4= +github.com/microsoftgraph/msgraph-sdk-go-core v1.0.1 h1:uq4qZD8VXLiNZY0t4NoRpLDoEiNYJvAQK3hc0ZMmdxs= +github.com/microsoftgraph/msgraph-sdk-go-core v1.0.1/go.mod h1:HUITyuFN556+0QZ/IVfH5K4FyJM7kllV6ExKi2ImKhE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb h1:GRiLv4rgyqjqzxbhJke65IYUf4NCOOvrPOJbV/sPxkM= github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb/go.mod h1:OaY7UOoTkkrX3wRwjpYRKafIkkyeD0UtweSHAWWiqQM= -github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= -github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mittwald/vaultgo v0.1.1 h1:85ONjtO8MX12kqWi4DoS+5eFa0o49QfGW4r2kKs+J9Y= -github.com/mittwald/vaultgo v0.1.1/go.mod h1:RoxE/B280Zw/9BsyqdzQSSYCWxu/fd/IkC0J8HfOsQ8= +github.com/mittwald/vaultgo v0.1.4 h1:f+r0H+hgzXL9b9hkOhkFAQMAR3a/RNgqzlqywN+StXo= +github.com/mittwald/vaultgo v0.1.4/go.mod h1:MuFKjvIXDjRU8cVxAKS/12JcxxzRCWzbdDcPC8sGdQQ= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/sys/mount v0.2.0 h1:WhCW5B355jtxndN5ovugJlMFJawbUODuW8fSnEH6SSM= -github.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7sxOougM= -github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= -github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= -github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= -github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= -github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/moby/sys/mount v0.3.3 h1:fX1SVkXFJ47XWDoeFW4Sq7PdQJnV2QIDZAqjNqgEjUs= +github.com/moby/sys/mount v0.3.3/go.mod h1:PBaEorSNTLG5t/+4EgukEQVlAvVEc6ZjTySwKdqp5K0= +github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= +github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1454,214 +877,74 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= -github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= +github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= -github.com/mwitkow/go-proto-validators v0.2.0/go.mod h1:ZfA1hW+UH/2ZHOWvQ3HnQaU0DtnpXu850MZiy+YUgcc= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso= -github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc93 h1:x2UMpOOVf3kQ8arv/EsDGwim8PTNqzL1/EYDr/+scOM= -github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= -github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= -github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= -github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= +github.com/opencontainers/runc v1.1.3/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= github.com/openfga/go-sdk v0.2.2 h1:zzQPdcX/CNLXwycqYNx5LvP78kzVs6R8p5GXw/0II3s= github.com/openfga/go-sdk v0.2.2/go.mod h1:ZB13O8GilPc0ITWssOszgxmz6CnIe8PQLZqbqAnx2IY= -github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= -github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= -github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/oracle/oci-go-sdk/v65 v65.49.0 h1:A/G4SuzLixNy43DsXj9Vok9TygRZRX15I62ebGTHj2Y= -github.com/oracle/oci-go-sdk/v65 v65.49.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= -github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= -github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= -github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= -github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= -github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/oracle/oci-go-sdk/v65 v65.55.0 h1:enKyHVLdJYDJrc9232w33u5F6t2p8Din4593kn3nh/w= +github.com/oracle/oci-go-sdk/v65 v65.55.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= github.com/packethost/packngo v0.28.1 h1:2Bo64Ku3F869oUmE2IrnURanN0XAmZmhI8wTem2sq64= github.com/packethost/packngo v0.28.1/go.mod h1:/UHguFdPs6Lf6FOkkSEPnRY5tgS0fsVM+Zv/bvBrmt0= -github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= -github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= -github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= -github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= -github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= -github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= +github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= +github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= +github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= +github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.0.0-20161124155732-575f371f7862/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= -github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= -github.com/prometheus/common v0.24.0/go.mod h1:H6QK/N6XVT42whUeIdI3dp36w49c+/iMDk7UAI2qm7Q= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk= -github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/pseudomuto/protoc-gen-doc v1.4.1/go.mod h1:exDTOVwqpp30eV/EDPFLZy3Pwr2sn6hBC1WIYH/UbIg= -github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rivo/tview v0.0.0-20220610163003-691f46d6f500 h1:KvoRB2TMfMqK2NF2mIvZprDT/Ofvsa4RphWLoCmUDag= github.com/rivo/tview v0.0.0-20220610163003-691f46d6f500/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -1670,39 +953,32 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a h1:3QH7VyOaaiUHNrA9Se4YQIRkDTCw1EJls9xTUCaCeRM= github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= -github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= -github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= @@ -1710,81 +986,38 @@ github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhr github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= -github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= -github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/soheilhy/cmux v0.1.5-0.20210205191134-5ec6847320e5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.3.4/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= -github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= -github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= -github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= -github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= -github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= -github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= +github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= +github.com/std-uritemplate/std-uritemplate/go v0.0.47 h1:erzz/DR4sOzWr0ca2MgSTkMckpLEsDySaTZwVFQq9zw= +github.com/std-uritemplate/std-uritemplate/go v0.0.47/go.mod h1:Qov4Ay4U83j37XjgxMYevGJFLbnZ2o9cEOhGufBKgKY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1794,136 +1027,43 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= -github.com/testcontainers/testcontainers-go v0.11.1 h1:FiYsB83LSGbiawoV8TpAZGfcCUbtaeeg1SXqEKUxh08= -github.com/testcontainers/testcontainers-go v0.11.1/go.mod h1:/V0UVq+1e7NWYoqTPog179clf0Qp9TOyp4EcXaEFQz8= -github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= -github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= -github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= -github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= -github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= -github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= -github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= -github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/testcontainers/testcontainers-go v0.15.0 h1:3Ex7PUGFv0b2bBsdOv6R42+SK2qoZnWBd21LvZYhUtQ= +github.com/testcontainers/testcontainers-go v0.15.0/go.mod h1:PkohMRH2X8Hib0IWtifVexDfLPVT+tb5E9hsf7cW12w= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= -github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -github.com/vmware/govmomi v0.21.1-0.20191008161538-40aebf13ba45 h1:zpQBW+l4uPQTfTOxedN5GEcSONhabbCf3X+5+P/H4Jk= -github.com/vmware/govmomi v0.21.1-0.20191008161538-40aebf13ba45/go.mod h1:zbnFoBQ9GIjs2RVETy8CNEpb+L+Lwkjs3XZUL0B3/m0= -github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk= -github.com/weppos/publicsuffix-go v0.13.1-0.20210123135404-5fd73613514e/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= -github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= -github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= -github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= -github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +github.com/vmware/govmomi v0.34.1 h1:Hqu2Uke2itC+cNoIcFQBLEZvX9wBRTTOP04J7V1fqRw= +github.com/vmware/govmomi v0.34.1/go.mod h1:qWWT6n9mdCr/T9vySsoUqcI04sSEj4CqHXxtk/Y+Los= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yohcop/openid-go v1.0.0 h1:EciJ7ZLETHR3wOtxBvKXx9RV6eyHZpCaSZ1inbBaUXE= github.com/yohcop/openid-go v1.0.0/go.mod h1:/408xiwkeItSPJZSTPF7+VtZxPkPrRRpRNK2vjGh6yI= -github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= -github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -github.com/zitadel/oidc/v2 v2.6.4 h1:bruA+KOFHcGpxr++WgtvR82ZlH54kKituu5xE4wpF7o= -github.com/zitadel/oidc/v2 v2.6.4/go.mod h1:owrsdzRqGvIZjBCY9LY1ZUYJ0mRUbGkQpZ3OskXL4wM= -github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= -github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= -github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= -github.com/zmap/zcrypto v0.0.0-20210123152837-9cf5beac6d91/go.mod h1:R/deQh6+tSWlgI9tb4jNmXxn8nSCabl5ZQsBX9//I/E= -github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk= -github.com/zmap/zlint/v3 v3.1.0/go.mod h1:L7t8s3sEKkb0A2BxGy1IWrxt1ZATa1R4QfJZaQOD3zU= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= -go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= -go.etcd.io/etcd/api/v3 v3.5.0-alpha.0/go.mod h1:mPcW6aZJukV6Aa81LSKpBjQXTWlXB5r74ymPoSWa3Sw= +github.com/zitadel/oidc/v2 v2.12.0 h1:4aMTAy99/4pqNwrawEyJqhRb3yY3PtcDxnoDSryhpn4= +github.com/zitadel/oidc/v2 v2.12.0/go.mod h1:LrRav74IiThHGapQgCHZOUNtnqJG0tcZKHro/91rtLw= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/api/v3 v3.5.9 h1:4wSsluwyTbGGmyjJktOf3wFQoTBIURXHnq9n/G/JQHs= -go.etcd.io/etcd/api/v3 v3.5.9/go.mod h1:uyAal843mC8uUVSLWz6eHa/d971iDGnCRpmKd2Z+X8k= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/pkg/v3 v3.5.9 h1:oidDC4+YEuSIQbsR94rY9gur91UPL6DnxDCIYd2IGsE= -go.etcd.io/etcd/client/pkg/v3 v3.5.9/go.mod h1:y+CzeSmkMpWN2Jyu1npecjB9BBnABxGM4pN8cGuJeL4= -go.etcd.io/etcd/client/v2 v2.305.0-alpha.0/go.mod h1:kdV+xzCJ3luEBSIeQyB/OEKkWKd8Zkux4sbDeANrosU= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v2 v2.305.7 h1:AELPkjNR3/igjbO7CjyF1fPuVPjrblliiKj+Y6xSGOU= -go.etcd.io/etcd/client/v2 v2.305.7/go.mod h1:GQGT5Z3TBuAQGvgPfhR7VPySu/SudxmEkRq9BgzFU6s= -go.etcd.io/etcd/client/v3 v3.5.0-alpha.0/go.mod h1:wKt7jgDgf/OfKiYmCq5WFGxOFAkVMLxiiXgLDFhECr8= -go.etcd.io/etcd/client/v3 v3.5.9 h1:r5xghnU7CwbUxD/fbUtRyJGaYNfDun8sp/gTr1hew6E= -go.etcd.io/etcd/client/v3 v3.5.9/go.mod h1:i/Eo5LrZ5IKqpbtpPDuaUnDOUv471oDg8cjQaUr2MbA= -go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0 h1:odMFuQQCg0UmPd7Cyw6TViRYv9ybGuXuki4CusDSzqA= -go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0/go.mod h1:YPwSaBciV5G6Gpt435AasAG3ROetZsKNUzibRa/++oo= -go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0 h1:3yLUEC0nFCxw/RArImOyRUI4OAFbg4PFpBbAhSNzKNY= -go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0/go.mod h1:tV31atvwzcybuqejDoY3oaNRTtlD2l/Ot78Pc9w7DMY= -go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0 h1:DvYJotxV9q1Lkn7pknzAbFO/CLtCVidCr2K9qRLJ8pA= -go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0/go.mod h1:FAwse6Zlm5v4tEWZaTjmNhe17Int4Oxbu7+2r0DiD3w= -go.etcd.io/etcd/server/v3 v3.5.0-alpha.0 h1:fYv7CmmdyuIu27UmKQjS9K/1GtcCa+XnPKqiKBbQkrk= -go.etcd.io/etcd/server/v3 v3.5.0-alpha.0/go.mod h1:tsKetYpt980ZTpzl/gb+UOJj9RkIyCb1u4wjzMg90BQ= -go.etcd.io/etcd/tests/v3 v3.5.0-alpha.0 h1:UcRoCA1FgXoc4CEM8J31fqEvI69uFIObY5ZDEFH7Znc= -go.etcd.io/etcd/tests/v3 v3.5.0-alpha.0/go.mod h1:HnrHxjyCuZ8YDt8PYVyQQ5d1ZQfzJVEtQWllr5Vp/30= -go.etcd.io/etcd/v3 v3.5.0-alpha.0 h1:ZuqKJkD2HrzFUj8IB+GLkTMKZ3+7mWx172vx6F1TukM= -go.etcd.io/etcd/v3 v3.5.0-alpha.0/go.mod h1:JZ79d3LV6NUfPjUxXrpiFAYcjhT+06qqw+i28snx8To= -go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= -go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -1933,87 +1073,65 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= -go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= -go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= -go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= -go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= -go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/mock v0.2.0 h1:TaP3xedm7JaAgScZO7tlvlKrqT0p7I6OsdGB5YNSMDU= -go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= -gocloud.dev v0.19.0/go.mod h1:SmKwiR8YwIMMJvQBKLsC3fHNyMwXLw3PMDO+VVteJMI= -golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -2024,9 +1142,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -2049,28 +1166,20 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -2078,17 +1187,10 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -2096,14 +1198,11 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -2111,30 +1210,22 @@ golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2145,18 +1236,12 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -2165,21 +1250,13 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2187,39 +1264,19 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190620070143-6f217b454f45/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191119060738-e882bf8e40c2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2227,81 +1284,56 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2312,53 +1344,39 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181008205924-a2b3f7f249e9/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191118222007-07fc4c7f2b98/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -2376,51 +1394,36 @@ golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201014170642-d1624618ad65/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= -golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= @@ -2439,33 +1442,22 @@ google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjR google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA= -google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o= -google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +google.golang.org/api v0.154.0 h1:X7QkVKZBskztmpPKWQXgjJRPA2dJYrL6r+sYPRLj050= +google.golang.org/api v0.154.0/go.mod h1:qhSMkM85hgqiokIYsrRyKxrjfBeIhgl4Z2JmeRkYylc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= -google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190620144150-6af8c5fc6601/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= @@ -2474,7 +1466,6 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= @@ -2483,7 +1474,6 @@ google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= @@ -2494,43 +1484,26 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210331142528-b7513248f0ba/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210510173355-fb37daa5cd7a/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8= -google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y= -google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529 h1:s5YSX+ZH5b5vS9rnpGymvIyMpLRJizowqDlOuyjXnTk= -google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= -google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f h1:Vn+VyHU5guc9KjB5KrjI2q0wCOWEOIh0OEsleqakHJg= +google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f/go.mod h1:nWSwAFPb+qfNJXsoeO3Io7zf4tMSfN8EA8RlDA04GhY= +google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY= +google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -2540,19 +1513,15 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -2563,45 +1532,31 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= -gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v1 v1.0.0-20161222125816-442357a80af5/go.mod h1:u0ALmqvLRxLI95fkdCEWrE6mhWYZW1aMOJHp5YXLHTg= gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso= gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/gobwas/glob.v0 v0.2.3 h1:uLMy+ys6BqRCutdUNyWLlmEnd7VULqh1nsxxV1kj0qQ= gopkg.in/gobwas/glob.v0 v0.2.3/go.mod h1:JgYsZg6HmXzPbMVcSQwXigfIbVWt5ysj8n78j6LiwQY= gopkg.in/httprequest.v1 v1.1.1/go.mod h1:/CkavNL+g3qLOrpFHVrEx4NKepeqR4XTZWNj4sGGjz0= -gopkg.in/httprequest.v1 v1.1.2/go.mod h1:/CkavNL+g3qLOrpFHVrEx4NKepeqR4XTZWNj4sGGjz0= gopkg.in/httprequest.v1 v1.2.0/go.mod h1:T61ZUaJLpMnzvoJDO03ZD8yRXD4nZzBeDoW5e9sffjg= gopkg.in/httprequest.v1 v1.2.1 h1:pEPLMdF/gjWHnKxLpuCYaHFjc8vAB2wrYjXrqDVC16E= gopkg.in/httprequest.v1 v1.2.1/go.mod h1:x2Otw96yda5+8+6ZeWwHIJTFkEHWP/qP8pJOzqEtWPM= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -2616,27 +1571,17 @@ gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4/go.mod h1:yeKp02qBN3iKW1OzL3M gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= -gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/retry.v1 v1.0.2/go.mod h1:tLRIBNXxoKtalyAWBSIbHdWkIBN2x9jVEm5l0Z+BjXs= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/retry.v1 v1.0.3 h1:a9CArYczAVv6Qs6VGoLMio99GEs7kY9UzSF9+LD+iGs= gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g= -gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= -gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= -gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs= gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -2645,12 +1590,8 @@ gorm.io/driver/postgres v1.0.5/go.mod h1:qrD92UurYzNctBMVCJ8C3VQEjffEuphycXtxOud gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.20.6 h1:qa7tC1WcU+DBI/ZKMxvXy1FcrlGsvxlaKufHrT2qQ08= gorm.io/gorm v1.20.6/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -2658,64 +1599,28 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.1.4/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= -k8s.io/api v0.21.10/go.mod h1:5kqv2pCXwcrOvV12WhVAtLZUKaM0kyrZ6nHObw8SojA= -k8s.io/api v0.23.4 h1:85gnfXQOWbJa1SiWGpE9EEtHs0UVvDyIsSMpEtl2D4E= -k8s.io/api v0.23.4/go.mod h1:i77F4JfyNNrhOjZF7OwwNJS5Y1S9dpwvb9iYRYRczfI= -k8s.io/apiextensions-apiserver v0.21.10 h1:61ymf3Yw6dadgfUbWCEVud5j6l9rme8ocy6jJbuFK04= -k8s.io/apiextensions-apiserver v0.21.10/go.mod h1:Uu9eBo+d489/K5pauF1oLaVxQzgvdng6aIb2LRlT/w8= -k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.21.10/go.mod h1:USs+ifLG6ZUgHGA/9lGxjdHzCB3hUO3fG1VBOwi0IHo= -k8s.io/apimachinery v0.23.4 h1:fhnuMd/xUL3Cjfl64j5ULKZ1/J9n8NuQEgNL+WXWfdM= -k8s.io/apimachinery v0.23.4/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= -k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= -k8s.io/apiserver v0.21.10/go.mod h1:dMEmFJ//OIDnnWmjcpVq+XkKQubRY1rAm5as7MwbIWQ= -k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= -k8s.io/client-go v0.21.10/go.mod h1:nAGhVCjwhbDP2whk65n3STSCn24H/VGp1pKSk9UszU8= -k8s.io/client-go v0.23.4 h1:YVWvPeerA2gpUudLelvsolzH7c2sFoXXR5wM/sWqNFU= -k8s.io/client-go v0.23.4/go.mod h1:PKnIL4pqLuvYUK1WU7RLTMYKPiIh7MYShLshtRY9cj0= -k8s.io/code-generator v0.21.10/go.mod h1:FbZCzn44pBTAjY3tIvLIQcF2514rGUzMP1liffRr5jQ= -k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= -k8s.io/component-base v0.21.10/go.mod h1:zO/BjKH/RR6DoZEQAws7m+Q81pYZ+f5vDfujDqvWdlA= -k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= -k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= -k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -k8s.io/kube-openapi v0.0.0-20211110012726-3cc51fd1e909/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= -k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210521133846-da695404a2bc/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20230711102312-30195339c3c7 h1:ZgnF1KZsYxWIifwSNZFZgNtWE89WI5yiP5WwlfDoIyc= -k8s.io/utils v0.0.0-20230711102312-30195339c3c7/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= +k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= +k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= +k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= +k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= +k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= +k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= +k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI= +k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= launchpad.net/xmlpath v0.0.0-20130614043138-000000000004/go.mod h1:vqyExLOM3qBx7mvYRkoxjSCF945s0mbe7YynlKYXtsA= -pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.27/go.mod h1:tq2nT0Kx7W+/f2JVE+zxYtUhdjuELJkVpNz+x/QN5R4= -sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= -sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= -sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/internal/auth/auth.go b/internal/auth/auth.go deleted file mode 100644 index 6aa1b3ebd..000000000 --- a/internal/auth/auth.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2024 canonical. - -// Package auth provides means to authenticate users into JIMM. -// -// The methods of authentication are: -// - Macaroons (deprecated) -// - OAuth2.0 (Device flow) -// - OAuth2.0 (Browser flow) -// - JWTs (For CLI based sessions) -package auth diff --git a/internal/auth/client.go b/internal/auth/client.go deleted file mode 100644 index 4d86818df..000000000 --- a/internal/auth/client.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2021 canonical. - -package auth - -import ( - "context" - - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" - bakeryv2 "gopkg.in/macaroon-bakery.v2/bakery" - identcheckerv2 "gopkg.in/macaroon-bakery.v2/bakery/identchecker" -) - -// IdentityClientV3 "upgrades" a v2 IdentityClient to a v3 one. -type IdentityClientV3 struct { - IdentityClient identcheckerv2.IdentityClient -} - -// IdentityFromContext implements IdentityClient. -func (c IdentityClientV3) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) { - var id identchecker.Identity - var cavs []checkers.Caveat - id2, cavs2, err := c.IdentityClient.IdentityFromContext(ctx) - if id2 != nil { - id = id2.(identchecker.Identity) - } - if cavs2 != nil { - cavs = make([]checkers.Caveat, len(cavs2)) - } - for i, cav2 := range cavs2 { - cavs[i] = checkers.Caveat{ - Condition: cav2.Condition, - Namespace: cav2.Namespace, - Location: cav2.Location, - } - } - return id, cavs, err -} - -// DeclaredIdentity implements IdentityClient. -func (c IdentityClientV3) DeclaredIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) { - var id identchecker.Identity - id2, err := c.IdentityClient.DeclaredIdentity(ctx, declared) - if id2 != nil { - id = id2.(identchecker.Identity) - } - return id, err -} - -// A ThirdPartyLocatorV3 adapts a v2 ThirdPartyLocator to a v3 one. -type ThirdPartyLocatorV3 struct { - ThirdPartyLocator bakeryv2.ThirdPartyLocator -} - -// ThirdPartyInfo implements ThirdPartyLocator. -func (l ThirdPartyLocatorV3) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) { - var tpi bakery.ThirdPartyInfo - tpi2, err := l.ThirdPartyLocator.ThirdPartyInfo(ctx, loc) - tpi.PublicKey.Key = bakery.Key(tpi2.PublicKey.Key) - tpi.Version = bakery.Version(tpi.Version) - return tpi, err -} diff --git a/internal/auth/jujuauth.go b/internal/auth/jujuauth.go index 97e0fac60..6e8abbfbf 100644 --- a/internal/auth/jujuauth.go +++ b/internal/auth/jujuauth.go @@ -3,18 +3,7 @@ package auth import ( - "context" - "fmt" - - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" - - "github.com/canonical/jimm/internal/dbmodel" - "github.com/canonical/jimm/internal/errors" - "github.com/canonical/jimm/internal/openfga" - "github.com/canonical/jimm/internal/servermon" ) // An AuthenticationError is the error returned when the requested @@ -28,55 +17,3 @@ type AuthenticationError struct { func (*AuthenticationError) Error() string { return "authentication failed" } - -// A JujuAuthenticator is an authenticator implementation using macaroons. -type JujuAuthenticator struct { - Bakery *identchecker.Bakery - ControllerAdmins []string - Client *openfga.OFGAClient -} - -// Authenticate implements jimm.Authenticator. -func (a JujuAuthenticator) Authenticate(ctx context.Context, req *jujuparams.LoginRequest) (*openfga.User, error) { - const op = errors.Op("auth.Authenticate") - if a.Client == nil { - return nil, errors.E(op, errors.CodeServerConfiguration, "openfga client not configured") - } - if a.Bakery == nil { - return nil, errors.E(op, errors.CodeServerConfiguration, "bakery not configured") - } - authInfo, err := a.Bakery.Checker.Auth(req.Macaroons...).Allow(ctx, identchecker.LoginOp) - if err != nil { - if derr, ok := err.(*bakery.DischargeRequiredError); ok { - // Return a discharge required response. - m, err := a.Bakery.Oven.NewMacaroon(ctx, req.BakeryVersion, derr.Caveats, derr.Ops...) - if err != nil { - return nil, errors.E(op, err) - } - return nil, &AuthenticationError{ - LoginResult: jujuparams.LoginResult{ - DischargeRequired: m.M(), - BakeryDischargeRequired: m, - DischargeRequiredReason: derr.Error(), - }, - } - } - servermon.AuthenticationFailCount.Inc() - return nil, errors.E(op, err) - } - if !names.IsValidUser(authInfo.Identity.Id()) { - return nil, errors.E(op, fmt.Sprintf("authenticated identity %q cannot be used as juju username", authInfo.Identity.Id())) - } - ut := names.NewUserTag(authInfo.Identity.Id()) - if ut.IsLocal() { - ut = ut.WithDomain("external") - } - u := &dbmodel.Identity{ - Name: ut.Id(), - DisplayName: ut.Name(), - } - // Note: Previously here we would grant a user superuser permission if they were part of - // a Launchpad group configurd in JIMM's config to grant superuser permission. - // We can no longer do this in the same way with OpenFGA. - return openfga.NewUser(u, a.Client), nil -} diff --git a/internal/auth/jujuauth_test.go b/internal/auth/jujuauth_test.go deleted file mode 100644 index 73620ce50..000000000 --- a/internal/auth/jujuauth_test.go +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright 2021 canonical. - -package auth_test - -import ( - "context" - "database/sql" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakerytest" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" - jujuparams "github.com/juju/juju/rpc/params" - "gopkg.in/macaroon.v2" - - "github.com/canonical/jimm/internal/auth" - "github.com/canonical/jimm/internal/dbmodel" - "github.com/canonical/jimm/internal/errors" - "github.com/canonical/jimm/internal/jimmtest" -) - -func TestAuthenticateLogin(t *testing.T) { - c := qt.New(t) - - ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) - c.Assert(err, qt.IsNil) - - discharger := bakerytest.NewDischarger(nil) - c.Cleanup(discharger.Close) - discharger.CheckerP = httpbakery.ThirdPartyCaveatCheckerPFunc( - func(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { - return []checkers.Caveat{checkers.DeclaredCaveat("username", "alice")}, nil - }, - ) - authenticator := auth.JujuAuthenticator{ - Client: ofgaClient, - Bakery: identchecker.NewBakery(identchecker.BakeryParams{ - Locator: discharger, - Key: bakery.MustGenerateKey(), - IdentityClient: testIdentityClient{loc: discharger.Location()}, - Location: "jimm", - Logger: testLogger{t: c}, - }), - } - - ctx := context.Background() - u, err := authenticator.Authenticate(ctx, &jujuparams.LoginRequest{}) - c.Check(u, qt.IsNil) - aerr, ok := err.(*auth.AuthenticationError) - c.Assert(ok, qt.Equals, true, qt.Commentf("unexpected error %s", err)) - - client := httpbakery.NewClient() - ms, err := client.DischargeAll(ctx, aerr.LoginResult.BakeryDischargeRequired) - c.Assert(err, qt.IsNil) - u, err = authenticator.Authenticate(ctx, &jujuparams.LoginRequest{Macaroons: []macaroon.Slice{ms}}) - c.Assert(err, qt.IsNil) - c.Check(u.LastLogin.Valid, qt.Equals, false) - u.LastLogin = sql.NullTime{} - c.Check(u.Identity, qt.DeepEquals, &dbmodel.Identity{ - Name: "alice@external", - DisplayName: "alice", - }) -} - -func TestAuthenticateLoginWithDomain(t *testing.T) { - c := qt.New(t) - - ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) - c.Assert(err, qt.IsNil) - - discharger := bakerytest.NewDischarger(nil) - c.Cleanup(discharger.Close) - discharger.CheckerP = httpbakery.ThirdPartyCaveatCheckerPFunc( - func(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { - return []checkers.Caveat{checkers.DeclaredCaveat("username", "alice@mydomain")}, nil - }, - ) - authenticator := auth.JujuAuthenticator{ - Client: ofgaClient, - Bakery: identchecker.NewBakery(identchecker.BakeryParams{ - Locator: discharger, - Key: bakery.MustGenerateKey(), - IdentityClient: testIdentityClient{loc: discharger.Location()}, - Location: "jimm", - Logger: testLogger{t: c}, - }), - } - - ctx := context.Background() - u, err := authenticator.Authenticate(ctx, &jujuparams.LoginRequest{}) - c.Check(u, qt.IsNil) - aerr, ok := err.(*auth.AuthenticationError) - c.Assert(ok, qt.Equals, true, qt.Commentf("unexpected error %s", err)) - - client := httpbakery.NewClient() - ms, err := client.DischargeAll(ctx, aerr.LoginResult.BakeryDischargeRequired) - c.Assert(err, qt.IsNil) - u, err = authenticator.Authenticate(ctx, &jujuparams.LoginRequest{Macaroons: []macaroon.Slice{ms}}) - c.Assert(err, qt.IsNil) - c.Check(u.LastLogin.Valid, qt.Equals, false) - u.LastLogin = sql.NullTime{} - c.Check(u.Identity, qt.DeepEquals, &dbmodel.Identity{ - Name: "alice@mydomain", - DisplayName: "alice", - }) -} - -func TestAuthenticateLoginSuperuser(t *testing.T) { - c := qt.New(t) - - ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) - c.Assert(err, qt.IsNil) - - discharger := bakerytest.NewDischarger(nil) - c.Cleanup(discharger.Close) - discharger.CheckerP = httpbakery.ThirdPartyCaveatCheckerPFunc( - func(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { - return []checkers.Caveat{checkers.DeclaredCaveat("username", "bob")}, nil - }, - ) - authenticator := auth.JujuAuthenticator{ - Client: ofgaClient, - Bakery: identchecker.NewBakery(identchecker.BakeryParams{ - Locator: discharger, - Key: bakery.MustGenerateKey(), - IdentityClient: testIdentityClient{loc: discharger.Location()}, - Location: "jimm", - Logger: testLogger{t: c}, - }), - ControllerAdmins: []string{"bob"}, - } - - ctx := context.Background() - u, err := authenticator.Authenticate(ctx, &jujuparams.LoginRequest{}) - c.Check(u, qt.IsNil) - aerr, ok := err.(*auth.AuthenticationError) - c.Assert(ok, qt.Equals, true, qt.Commentf("unexpected error %s", err)) - - client := httpbakery.NewClient() - ms, err := client.DischargeAll(ctx, aerr.LoginResult.BakeryDischargeRequired) - c.Assert(err, qt.IsNil) - u, err = authenticator.Authenticate(ctx, &jujuparams.LoginRequest{Macaroons: []macaroon.Slice{ms}}) - c.Assert(err, qt.IsNil) - c.Check(u.LastLogin.Valid, qt.Equals, false) - u.LastLogin = sql.NullTime{} - c.Check(u.Identity, qt.DeepEquals, &dbmodel.Identity{ - Name: "bob@external", - DisplayName: "bob", - }) -} - -func TestAuthenticateLoginInvalidUsernameDeclared(t *testing.T) { - c := qt.New(t) - - ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) - c.Assert(err, qt.IsNil) - - discharger := bakerytest.NewDischarger(nil) - c.Cleanup(discharger.Close) - discharger.CheckerP = httpbakery.ThirdPartyCaveatCheckerPFunc( - func(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { - return []checkers.Caveat{checkers.DeclaredCaveat("username", "A")}, nil - }, - ) - authenticator := auth.JujuAuthenticator{ - Client: ofgaClient, - Bakery: identchecker.NewBakery(identchecker.BakeryParams{ - Locator: discharger, - Key: bakery.MustGenerateKey(), - IdentityClient: testIdentityClient{loc: discharger.Location()}, - Location: "jimm", - Logger: testLogger{t: c}, - }), - } - - ctx := context.Background() - u, err := authenticator.Authenticate(ctx, &jujuparams.LoginRequest{}) - c.Check(u, qt.IsNil) - aerr, ok := err.(*auth.AuthenticationError) - c.Assert(ok, qt.Equals, true, qt.Commentf("unexpected error %s", err)) - - client := httpbakery.NewClient() - ms, err := client.DischargeAll(ctx, aerr.LoginResult.BakeryDischargeRequired) - c.Assert(err, qt.IsNil) - _, err = authenticator.Authenticate(ctx, &jujuparams.LoginRequest{Macaroons: []macaroon.Slice{ms}}) - c.Assert(err, qt.ErrorMatches, `authenticated identity "A" cannot be used as juju username`) -} - -type testIdentityClient struct { - loc string -} - -func (c testIdentityClient) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) { - cav := checkers.Caveat{ - Condition: "is-authenticated-user", - Location: c.loc, - } - return nil, []checkers.Caveat{checkers.NeedDeclaredCaveat(cav, "username")}, nil -} - -func (testIdentityClient) DeclaredIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) { - if username, ok := declared["username"]; ok { - return identchecker.SimpleIdentity(username), nil - } - return nil, errors.E("username not declared") -} - -type testLogger struct { - t testing.TB -} - -func (l testLogger) Infof(_ context.Context, f string, args ...interface{}) { - l.t.Logf(f, args...) -} - -func (l testLogger) Debugf(_ context.Context, f string, args ...interface{}) { - l.t.Logf(f, args...) -} diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index e052e96c4..c98b7e802 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -1,5 +1,11 @@ // Copyright 2024 canonical. +// Package auth provides means to authenticate users into JIMM. +// +// The methods of authentication are: +// - OAuth2.0 (Device flow) +// - OAuth2.0 (Browser flow) +// - JWTs (For CLI based sessions) package auth import ( diff --git a/internal/cmdtest/jimmsuite.go b/internal/cmdtest/jimmsuite.go index 748ae8060..bc90bc5ec 100644 --- a/internal/cmdtest/jimmsuite.go +++ b/internal/cmdtest/jimmsuite.go @@ -7,23 +7,24 @@ package cmdtest import ( "bytes" "context" + "crypto/tls" "encoding/pem" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" + "strings" "time" cofga "github.com/canonical/ofga" "github.com/coreos/go-oidc/v3/oidc" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" "github.com/juju/juju/api" "github.com/juju/juju/core/network" corejujutesting "github.com/juju/juju/juju/testing" jjclient "github.com/juju/juju/jujuclient" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" service "github.com/canonical/jimm" @@ -36,7 +37,6 @@ import ( ) type JimmCmdSuite struct { - jimmtest.CandidSuite corejujutesting.JujuConnSuite Params service.Params @@ -56,9 +56,8 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { ctx, cancel := context.WithCancel(context.Background()) s.cancel = cancel - s.CandidSuite.SetUpTest(c) - s.HTTP = httptest.NewUnstartedServer(nil) + s.HTTP.TLS = setupTLS(c) u, err := url.Parse("https://" + s.HTTP.Listener.Addr().String()) c.Assert(err, gc.Equals, nil) @@ -71,8 +70,6 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { s.Params = service.Params{ PublicDNSName: u.Host, ControllerUUID: "914487b5-60e7-42bb-bd63-1adc3fd3a388", - CandidURL: s.Candid.URL.String(), - CandidPublicKey: s.CandidPublicKey, ControllerAdmins: []string{"admin"}, DSN: jimmtest.CreateEmptyDatabase(&jimmtest.GocheckTester{c}), OpenFGAParams: service.OpenFGAParams{ @@ -109,11 +106,10 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { s.ControllerConfigAttrs = map[string]interface{}{ "login-token-refresh-url": u.String() + "/.well-known/jwks.json", } - s.ControllerAdmins = []string{"controller-admin"} s.JujuConnSuite.SetUpTest(c) s.AdminUser = &dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", LastLogin: db.Now(), } err = s.JIMM.Database.GetIdentity(ctx, s.AdminUser) @@ -123,7 +119,7 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { err = alice.SetControllerAccess(context.Background(), s.JIMM.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, gc.Equals, nil) - s.Candid.AddUser("alice") + s.AddAdminUser(c, "alice@canonical.com") w := new(bytes.Buffer) err = pem.Encode(w, &pem.Block{ @@ -145,19 +141,6 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { } } -// RefreshControllerAddress is a useful helper function when writing table tests for JIMM CLI -// commands that use NewAPIRootWithDialOpts. Each invocation of the NewAPIRootWithDialOpts function -// updates the ClientStore and removes local IPs thus removing JIMM's IP. -// Call this function in your table tests after each test run. -func (s *JimmCmdSuite) RefreshControllerAddress(c *gc.C) { - jimm, ok := s.ClientStore().Controllers["JIMM"] - c.Assert(ok, gc.Equals, true) - u, err := url.Parse(s.HTTP.URL) - c.Assert(err, gc.IsNil) - jimm.APIEndpoints = []string{u.Host} - s.ClientStore().Controllers["JIMM"] = jimm -} - func (s *JimmCmdSuite) TearDownTest(c *gc.C) { if s.cancel != nil { s.cancel() @@ -165,29 +148,73 @@ func (s *JimmCmdSuite) TearDownTest(c *gc.C) { if s.HTTP != nil { s.HTTP.Close() } - if err := s.JIMM.Database.Close(); err != nil { - c.Logf("failed to close database connections at tear down: %s", err) + if s.JIMM != nil && s.JIMM.Database.DB != nil { + if err := s.JIMM.Database.Close(); err != nil { + c.Logf("failed to close database connections at tear down: %s", err) + } } - s.CandidSuite.TearDownTest(c) s.JujuConnSuite.TearDownTest(c) } -func (s *JimmCmdSuite) UserBakeryClient(username string) *httpbakery.Client { - s.Candid.AddUser(username) - key := s.Candid.UserPublicKey(username) - bClient := httpbakery.NewClient() - bClient.Key = &bakery.KeyPair{ - Public: bakery.PublicKey{Key: bakery.Key(key.Public.Key)}, - Private: bakery.PrivateKey{Key: bakery.Key(key.Private.Key)}, +func getRootJimmPath(c *gc.C) string { + path, err := os.Getwd() + c.Assert(err, gc.IsNil) + dirs := strings.Split(path, "/") + c.Assert(len(dirs), gc.Not(gc.Equals), 1) + dirs = dirs[1:] + jimmIndex := -1 + // Range over dirs from the end to ensure no top-level jimm + // folders interfere with our search. + for i := len(dirs) - 1; i >= 0; i-- { + if dirs[i] == "jimm" { + jimmIndex = i + 1 + break + } } - agent.SetUpAuth(bClient, &agent.AuthInfo{ - Key: bClient.Key, - Agents: []agent.Agent{{ - URL: s.Candid.URL.String(), - Username: username, - }}, - }) - return bClient + c.Assert(jimmIndex, gc.Not(gc.Equals), -1) + return "/" + filepath.Join(dirs[:jimmIndex]...) +} + +func setupTLS(c *gc.C) *tls.Config { + jimmPath := getRootJimmPath(c) + pathToCert := filepath.Join(jimmPath, "local/traefik/certs/server.crt") + localhostCert, err := os.ReadFile(pathToCert) + c.Assert(err, gc.IsNil, gc.Commentf("Unable to find cert at %s. Run make cert in root directory.", pathToCert)) + + pathToKey := filepath.Join(jimmPath, "local/traefik/certs/server.key") + localhostKey, err := os.ReadFile(pathToKey) + c.Assert(err, gc.IsNil, gc.Commentf("Unable to find key at %s. Run make cert in root directory.", pathToKey)) + + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + c.Assert(err, gc.IsNil, gc.Commentf("Failed to generate certificate key pair.")) + + tlsConfig := new(tls.Config) + tlsConfig.Certificates = []tls.Certificate{cert} + return tlsConfig +} + +func (s *JimmCmdSuite) AddAdminUser(c *gc.C, email string) { + identity := dbmodel.Identity{ + Name: email, + } + err := s.JIMM.Database.GetIdentity(context.Background(), &identity) + c.Assert(err, gc.IsNil) + ofgaUser := openfga.NewUser(&identity, s.OFGAClient) + err = ofgaUser.SetControllerAccess(context.Background(), s.JIMM.ResourceTag(), ofganames.AdministratorRelation) + c.Assert(err, gc.IsNil) +} + +// RefreshControllerAddress is a useful helper function when writing table tests for JIMM CLI +// commands that use NewAPIRootWithDialOpts. Each invocation of the NewAPIRootWithDialOpts function +// updates the ClientStore and removes local IPs thus removing JIMM's IP. +// Call this function in your table tests after each test run. +func (s *JimmCmdSuite) RefreshControllerAddress(c *gc.C) { + jimm, ok := s.ClientStore().Controllers["JIMM"] + c.Assert(ok, gc.Equals, true) + u, err := url.Parse(s.HTTP.URL) + c.Assert(err, gc.IsNil) + jimm.APIEndpoints = []string{u.Host} + s.ClientStore().Controllers["JIMM"] = jimm } func (s *JimmCmdSuite) AddController(c *gc.C, name string, info *api.Info) { diff --git a/internal/db/applicationoffer_test.go b/internal/db/applicationoffer_test.go index 752c30dd6..6d7a38d48 100644 --- a/internal/db/applicationoffer_test.go +++ b/internal/db/applicationoffer_test.go @@ -41,7 +41,7 @@ func initTestEnvironment(c *qt.C, db *db.Database) testEnvironment { env := testEnvironment{} env.u = dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(db.DB.Create(&env.u).Error, qt.IsNil) @@ -242,7 +242,7 @@ func (s *dbSuite) TestFindApplicationOffers(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) diff --git a/internal/db/auditlog_test.go b/internal/db/auditlog_test.go index 55d2606c4..13b8e8e36 100644 --- a/internal/db/auditlog_test.go +++ b/internal/db/auditlog_test.go @@ -8,7 +8,7 @@ import ( "time" qt "github.com/frankban/quicktest" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" @@ -29,7 +29,7 @@ func (s *dbSuite) TestAddAuditLogEntry(c *qt.C) { ale := dbmodel.AuditLogEntry{ Time: time.Now().UTC().Round(time.Millisecond), - IdentityTag: names.NewUserTag("alice@external").String(), + IdentityTag: names.NewUserTag("alice@canonical.com").String(), } err := s.Database.AddAuditLogEntry(ctx, &ale) @@ -64,16 +64,16 @@ func TestForEachAuditLogEntryUnconfiguredDatabase(t *testing.T) { var testAuditLogEntries = []dbmodel.AuditLogEntry{{ Time: time.Date(2020, time.February, 20, 20, 2, 20, 0, time.UTC), - IdentityTag: names.NewUserTag("alice@external").String(), + IdentityTag: names.NewUserTag("alice@canonical.com").String(), }, { Time: time.Date(2020, time.February, 20, 20, 2, 21, 0, time.UTC), - IdentityTag: names.NewUserTag("alice@external").String(), + IdentityTag: names.NewUserTag("alice@canonical.com").String(), }, { Time: time.Date(2020, time.February, 20, 20, 2, 21, 0, time.UTC), - IdentityTag: names.NewUserTag("bob@external").String(), + IdentityTag: names.NewUserTag("bob@canonical.com").String(), }, { Time: time.Date(2020, time.February, 20, 20, 2, 23, 0, time.UTC), - IdentityTag: names.NewUserTag("alice@external").String(), + IdentityTag: names.NewUserTag("alice@canonical.com").String(), }} var forEachAuditLogEntryTests = []struct { @@ -106,7 +106,7 @@ var forEachAuditLogEntryTests = []struct { }, { name: "UserTagFilter", filter: db.AuditLogFilter{ - IdentityTag: names.NewUserTag("alice@external").String(), + IdentityTag: names.NewUserTag("alice@canonical.com").String(), }, expectEntries: []int{0, 1, 3}, }} @@ -210,15 +210,15 @@ func (s *dbSuite) TestPurgeLogsFromDb(c *qt.C) { relativeNow := time.Now().AddDate(-1, 0, 0) ale := dbmodel.AuditLogEntry{ Time: relativeNow.UTC().Round(time.Millisecond), - IdentityTag: names.NewUserTag("alice@external").String(), + IdentityTag: names.NewUserTag("alice@canonical.com").String(), } ale_past := dbmodel.AuditLogEntry{ Time: relativeNow.AddDate(0, 0, -1).UTC().Round(time.Millisecond), - IdentityTag: names.NewUserTag("alice@external").String(), + IdentityTag: names.NewUserTag("alice@canonical.com").String(), } ale_future := dbmodel.AuditLogEntry{ Time: relativeNow.AddDate(0, 0, 5).UTC().Round(time.Millisecond), - IdentityTag: names.NewUserTag("alice@external").String(), + IdentityTag: names.NewUserTag("alice@canonical.com").String(), } err := s.Database.Migrate(context.Background(), false) diff --git a/internal/db/cloudcredential_test.go b/internal/db/cloudcredential_test.go index 0ad8f039e..837c357b7 100644 --- a/internal/db/cloudcredential_test.go +++ b/internal/db/cloudcredential_test.go @@ -9,7 +9,7 @@ import ( "testing" qt "github.com/frankban/quicktest" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" @@ -31,7 +31,7 @@ func (s *dbSuite) TestSetCloudCredentialInvalidTag(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -59,7 +59,7 @@ func (s *dbSuite) TestSetCloudCredential(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -96,7 +96,7 @@ func (s *dbSuite) TestSetCloudCredentialUpdate(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -166,7 +166,7 @@ func (s *dbSuite) TestGetCloudCredential(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -221,25 +221,25 @@ const forEachCloudCredentialEnv = `clouds: cloud-credentials: - name: cred-1 cloud: cloud-1 - owner: alice@external + owner: alice@canonical.com attributes: k1: v1 k2: v2 - name: cred-2 cloud: cloud-1 - owner: bob@external + owner: bob@canonical.com attributes: k1: v1 k2: v2 - name: cred-3 cloud: cloud-2 - owner: alice@external + owner: alice@canonical.com - name: cred-4 cloud: cloud-2 - owner: bob@external + owner: bob@canonical.com - name: cred-5 cloud: cloud-1 - owner: alice@external + owner: alice@canonical.com ` var forEachCloudCredentialTests = []struct { @@ -252,22 +252,22 @@ var forEachCloudCredentialTests = []struct { expectErrorCode errors.Code }{{ name: "UserCredentialsWithCloud", - username: "alice@external", + username: "alice@canonical.com", cloud: "cloud-1", expectCredentials: []string{ - names.NewCloudCredentialTag("cloud-1/alice@external/cred-1").String(), - names.NewCloudCredentialTag("cloud-1/alice@external/cred-5").String(), + names.NewCloudCredentialTag("cloud-1/alice@canonical.com/cred-1").String(), + names.NewCloudCredentialTag("cloud-1/alice@canonical.com/cred-5").String(), }, }, { name: "UserCredentialsWithoutCloud", - username: "bob@external", + username: "bob@canonical.com", expectCredentials: []string{ - names.NewCloudCredentialTag("cloud-1/bob@external/cred-2").String(), - names.NewCloudCredentialTag("cloud-2/bob@external/cred-4").String(), + names.NewCloudCredentialTag("cloud-1/bob@canonical.com/cred-2").String(), + names.NewCloudCredentialTag("cloud-2/bob@canonical.com/cred-4").String(), }, }, { name: "IterationError", - username: "alice@external", + username: "alice@canonical.com", f: func(*dbmodel.CloudCredential) error { return errors.E("test error", errors.Code("test code")) }, diff --git a/internal/db/clouddefaults.go b/internal/db/clouddefaults.go index 49c6d3e30..9faf105ab 100644 --- a/internal/db/clouddefaults.go +++ b/internal/db/clouddefaults.go @@ -5,7 +5,7 @@ package db import ( "context" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "gorm.io/gorm/clause" "github.com/canonical/jimm/internal/dbmodel" diff --git a/internal/db/clouddefaults_test.go b/internal/db/clouddefaults_test.go index dfbbb9b2c..408dfc11e 100644 --- a/internal/db/clouddefaults_test.go +++ b/internal/db/clouddefaults_test.go @@ -8,7 +8,7 @@ import ( qt "github.com/frankban/quicktest" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "gorm.io/gorm" "github.com/canonical/jimm/internal/db" @@ -23,7 +23,7 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) diff --git a/internal/db/controller_test.go b/internal/db/controller_test.go index df6caea9f..43120d0bd 100644 --- a/internal/db/controller_test.go +++ b/internal/db/controller_test.go @@ -123,7 +123,7 @@ func (s *dbSuite) TestGetControllerWithModels(c *qt.C) { CloudRegion: "test-region", } u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -219,7 +219,7 @@ const testForEachControllerEnv = `clouds: cloud-credentials: - name: test-cred cloud: test - owner: alice@external + owner: alice@canonical.com type: empty controllers: - name: test1 @@ -379,7 +379,7 @@ const testForEachControllerModelEnv = `clouds: cloud-credentials: - name: test-cred cloud: test - owner: alice@external + owner: alice@canonical.com type: empty controllers: - name: test @@ -392,28 +392,28 @@ controllers: region: test-region models: - name: test-1 - owner: alice@external + owner: alice@canonical.com uuid: 00000002-0000-0000-0000-000000000001 controller: test cloud: test region: test-region cloud-credential: test-cred - name: test-2 - owner: alice@external + owner: alice@canonical.com uuid: 00000002-0000-0000-0000-000000000002 controller: test cloud: test region: test-region cloud-credential: test-cred - name: test-3 - owner: alice@external + owner: alice@canonical.com uuid: 00000002-0000-0000-0000-000000000003 controller: test-2 cloud: test region: test-region cloud-credential: test-cred - name: test-4 - owner: alice@external + owner: alice@canonical.com uuid: 00000002-0000-0000-0000-000000000004 controller: test cloud: test diff --git a/internal/db/db_test.go b/internal/db/db_test.go index 0bb490e56..b89db0613 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -62,7 +62,7 @@ func (s *dbSuite) TestTransaction(c *qt.C) { err = s.Database.Transaction(func(d *db.Database) error { c.Check(d, qt.Not(qt.Equals), s.Database) - return d.GetIdentity(context.Background(), &dbmodel.Identity{Name: "bob@external"}) + return d.GetIdentity(context.Background(), &dbmodel.Identity{Name: "bob@canonical.com"}) }) c.Assert(err, qt.IsNil) diff --git a/internal/db/identity_test.go b/internal/db/identity_test.go index d23c61437..cfbcbf192 100644 --- a/internal/db/identity_test.go +++ b/internal/db/identity_test.go @@ -36,7 +36,7 @@ func (s *dbSuite) TestGetIdentity(c *qt.C) { c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } err = s.Database.GetIdentity(ctx, &u) c.Assert(err, qt.IsNil) @@ -72,7 +72,7 @@ func (s *dbSuite) TestUpdateIdentity(c *qt.C) { c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } err = s.Database.GetIdentity(ctx, &u) c.Assert(err, qt.IsNil) @@ -113,7 +113,7 @@ func (s *dbSuite) TestGetIdentityCloudCredentials(c *qt.C) { c.Check(err, qt.IsNil) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) diff --git a/internal/db/identitymodeldefaults_test.go b/internal/db/identitymodeldefaults_test.go index df3e6af4c..a3922f14a 100644 --- a/internal/db/identitymodeldefaults_test.go +++ b/internal/db/identitymodeldefaults_test.go @@ -38,7 +38,7 @@ func TestSetIdentityModelDefaults(t *testing.T) { about: "defaults do not exist yet - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { identity := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) @@ -63,7 +63,7 @@ func TestSetIdentityModelDefaults(t *testing.T) { about: "defaults already exist - defaults updated", setup: func(c *qt.C, j *jimm.JIMM) testConfig { identity := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) @@ -98,7 +98,7 @@ func TestSetIdentityModelDefaults(t *testing.T) { about: "identity does not exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { identity := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } defaults := map[string]interface{}{ @@ -117,7 +117,7 @@ func TestSetIdentityModelDefaults(t *testing.T) { about: "cannot set agent-version", setup: func(c *qt.C, j *jimm.JIMM) testConfig { identity := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) diff --git a/internal/db/model_test.go b/internal/db/model_test.go index 62b5a60e4..97105f555 100644 --- a/internal/db/model_test.go +++ b/internal/db/model_test.go @@ -32,7 +32,7 @@ func (s *dbSuite) TestAddModel(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -105,7 +105,7 @@ func (s *dbSuite) TestGetModel(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -205,7 +205,7 @@ func (s *dbSuite) TestUpdateModel(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -283,7 +283,7 @@ func (s *dbSuite) TestDeleteModel(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -357,7 +357,7 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { c.Assert(err, qt.Equals, nil) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) @@ -490,7 +490,7 @@ const testForEachModelEnv = `clouds: cloud-credentials: - name: test-cred cloud: test - owner: alice@external + owner: alice@canonical.com type: empty controllers: - name: test @@ -500,35 +500,35 @@ controllers: models: - name: test-1 uuid: 00000002-0000-0000-0000-000000000001 - owner: alice@external + owner: alice@canonical.com cloud: test region: test-region cloud-credential: test-cred controller: test users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: write - name: test-2 uuid: 00000002-0000-0000-0000-000000000002 - owner: bob@external + owner: bob@canonical.com cloud: test region: test-region cloud-credential: test-cred controller: test users: - - user: bob@external + - user: bob@canonical.com access: admin - name: test-3 uuid: 00000002-0000-0000-0000-000000000003 - owner: bob@external + owner: bob@canonical.com cloud: test region: test-region cloud-credential: test-cred controller: test users: - - user: bob@external + - user: bob@canonical.com access: admin ` @@ -567,7 +567,7 @@ const testGetModelsByUUIDEnv = `clouds: cloud-credentials: - name: test-cred cloud: test - owner: alice@external + owner: alice@canonical.com type: empty controllers: - name: test @@ -577,35 +577,35 @@ controllers: models: - name: test-1 uuid: 00000002-0000-0000-0000-000000000001 - owner: alice@external + owner: alice@canonical.com cloud: test region: test-region cloud-credential: test-cred controller: test users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: write - name: test-2 uuid: 00000002-0000-0000-0000-000000000002 - owner: bob@external + owner: bob@canonical.com cloud: test region: test-region cloud-credential: test-cred controller: test users: - - user: bob@external + - user: bob@canonical.com access: admin - name: test-3 uuid: 00000002-0000-0000-0000-000000000003 - owner: bob@external + owner: bob@canonical.com cloud: test region: test-region cloud-credential: test-cred controller: test users: - - user: bob@external + - user: bob@canonical.com access: admin ` diff --git a/internal/db/secrets.go b/internal/db/secrets.go index dee5d74b6..9cd9b129c 100644 --- a/internal/db/secrets.go +++ b/internal/db/secrets.go @@ -9,7 +9,7 @@ import ( "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "github.com/lestrrat-go/jwx/v2/jwk" "go.uber.org/zap" diff --git a/internal/db/secrets_test.go b/internal/db/secrets_test.go index 64512e2f1..71e2249f4 100644 --- a/internal/db/secrets_test.go +++ b/internal/db/secrets_test.go @@ -8,7 +8,7 @@ import ( qt "github.com/frankban/quicktest" "github.com/google/uuid" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" diff --git a/internal/dbmodel/applicationoffer.go b/internal/dbmodel/applicationoffer.go index 419d5901b..60f29dcdf 100644 --- a/internal/dbmodel/applicationoffer.go +++ b/internal/dbmodel/applicationoffer.go @@ -5,9 +5,9 @@ package dbmodel import ( "time" - "github.com/juju/charm/v11" + "github.com/juju/charm/v12" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "gorm.io/gorm" ) diff --git a/internal/dbmodel/applicationoffer_test.go b/internal/dbmodel/applicationoffer_test.go index 1586f0a55..1a4f80863 100644 --- a/internal/dbmodel/applicationoffer_test.go +++ b/internal/dbmodel/applicationoffer_test.go @@ -6,7 +6,7 @@ import ( "testing" qt "github.com/frankban/quicktest" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/dbmodel" ) diff --git a/internal/dbmodel/audit_test.go b/internal/dbmodel/audit_test.go index 6c52bacec..8b8dabb51 100644 --- a/internal/dbmodel/audit_test.go +++ b/internal/dbmodel/audit_test.go @@ -8,7 +8,7 @@ import ( "time" qt "github.com/frankban/quicktest" - "github.com/juju/names/v4" + "github.com/juju/names/v5" apiparams "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/dbmodel" @@ -30,7 +30,7 @@ func TestAuditLogEntry(t *testing.T) { FacadeMethod: "AddController", FacadeVersion: 1, ObjectId: "1", - IdentityTag: names.NewUserTag("bob@external").String(), + IdentityTag: names.NewUserTag("bob@canonical.com").String(), IsResponse: false, Params: paramsJSON, Errors: nil, @@ -57,7 +57,7 @@ func TestToAPIAuditEvent(t *testing.T) { FacadeMethod: "AddController", FacadeVersion: 1, ObjectId: "1", - IdentityTag: names.NewUserTag("bob@external").String(), + IdentityTag: names.NewUserTag("bob@canonical.com").String(), IsResponse: false, Params: paramsJSON, Errors: nil, @@ -71,7 +71,7 @@ func TestToAPIAuditEvent(t *testing.T) { FacadeMethod: "AddController", FacadeVersion: 1, ObjectId: "1", - UserTag: names.NewUserTag("bob@external").String(), + UserTag: names.NewUserTag("bob@canonical.com").String(), IsResponse: false, Params: map[string]any{"a": "b", "c": "d"}, Errors: nil, diff --git a/internal/dbmodel/cloud.go b/internal/dbmodel/cloud.go index 29c0118df..879238be2 100644 --- a/internal/dbmodel/cloud.go +++ b/internal/dbmodel/cloud.go @@ -6,7 +6,7 @@ import ( "time" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "gorm.io/gorm" ) diff --git a/internal/dbmodel/cloud_test.go b/internal/dbmodel/cloud_test.go index 0d3b2baf1..7d1351ec3 100644 --- a/internal/dbmodel/cloud_test.go +++ b/internal/dbmodel/cloud_test.go @@ -7,7 +7,7 @@ import ( qt "github.com/frankban/quicktest" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "gorm.io/gorm" "github.com/canonical/jimm/internal/dbmodel" diff --git a/internal/dbmodel/cloudcredential.go b/internal/dbmodel/cloudcredential.go index cc6d0d524..0ec0053ea 100644 --- a/internal/dbmodel/cloudcredential.go +++ b/internal/dbmodel/cloudcredential.go @@ -6,7 +6,7 @@ import ( "database/sql" "fmt" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "gorm.io/gorm" ) diff --git a/internal/dbmodel/cloudcredential_test.go b/internal/dbmodel/cloudcredential_test.go index 651e89817..73f2f7ee6 100644 --- a/internal/dbmodel/cloudcredential_test.go +++ b/internal/dbmodel/cloudcredential_test.go @@ -6,7 +6,7 @@ import ( "testing" qt "github.com/frankban/quicktest" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/dbmodel" ) @@ -37,7 +37,7 @@ func TestCloudCredential(t *testing.T) { Name: "test-cloud", }, Owner: dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", }, AuthType: "empty", Label: "test label", @@ -70,14 +70,14 @@ func TestCloudCredentialsCascadeOnDelete(t *testing.T) { Name: "test-credential", Cloud: cloud, Owner: dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", }, } result = db.Create(&cred) c.Assert(result.Error, qt.IsNil) c.Check(result.RowsAffected, qt.Equals, int64(1)) c.Check(cred.CloudName, qt.Equals, "test-cloud") - c.Check(cred.OwnerIdentityName, qt.Equals, "bob@external") + c.Check(cred.OwnerIdentityName, qt.Equals, "bob@canonical.com") result = db.Delete(&cloud) c.Assert(result.Error, qt.IsNil) diff --git a/internal/dbmodel/controller.go b/internal/dbmodel/controller.go index 3ceacca81..92297f36c 100644 --- a/internal/dbmodel/controller.go +++ b/internal/dbmodel/controller.go @@ -8,7 +8,7 @@ import ( "net" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "gorm.io/gorm" apiparams "github.com/canonical/jimm/api/params" diff --git a/internal/dbmodel/controller_test.go b/internal/dbmodel/controller_test.go index 720abb730..2ac7b7d75 100644 --- a/internal/dbmodel/controller_test.go +++ b/internal/dbmodel/controller_test.go @@ -10,7 +10,7 @@ import ( "github.com/canonical/jimm/internal/dbmodel" qt "github.com/frankban/quicktest" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" ) func TestControllerTag(t *testing.T) { @@ -92,7 +92,7 @@ func TestControllerModels(t *testing.T) { c.Assert(db.Create(&m1).Error, qt.IsNil) u2 := dbmodel.Identity{ - Name: "charlie@external", + Name: "charlie@canonical.com", } c.Assert(db.Create(&u2).Error, qt.IsNil) diff --git a/internal/dbmodel/group.go b/internal/dbmodel/group.go index b307b98c2..ecb41a869 100644 --- a/internal/dbmodel/group.go +++ b/internal/dbmodel/group.go @@ -6,7 +6,7 @@ import ( "strconv" "time" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "gorm.io/gorm" apiparams "github.com/canonical/jimm/api/params" diff --git a/internal/dbmodel/identity.go b/internal/dbmodel/identity.go index df69dd680..75607fc65 100644 --- a/internal/dbmodel/identity.go +++ b/internal/dbmodel/identity.go @@ -6,7 +6,7 @@ import ( "database/sql" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "gorm.io/gorm" ) @@ -15,7 +15,7 @@ type Identity struct { gorm.Model // Name is the name of the identity. This is the user name when - // representing a Juju user (i.e. with an @external suffix), or the client + // representing a Juju user (i.e. with an @canonical.com suffix), or the client // ID for a service account. The Name will have originated at an // external identity provider in JAAS deployments. Name string `gorm:"not null;uniqueIndex"` diff --git a/internal/dbmodel/identity_test.go b/internal/dbmodel/identity_test.go index d9b0132df..29828e100 100644 --- a/internal/dbmodel/identity_test.go +++ b/internal/dbmodel/identity_test.go @@ -9,7 +9,7 @@ import ( qt "github.com/frankban/quicktest" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "gorm.io/gorm" "github.com/canonical/jimm/internal/dbmodel" @@ -20,11 +20,11 @@ func TestIdentity(t *testing.T) { db := gormDB(c) var u0 dbmodel.Identity - result := db.Where("name = ?", "bob@external").First(&u0) + result := db.Where("name = ?", "bob@canonical.com").First(&u0) c.Check(result.Error, qt.Equals, gorm.ErrRecordNotFound) u1 := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", DisplayName: "bob", } result = db.Create(&u1) @@ -32,7 +32,7 @@ func TestIdentity(t *testing.T) { c.Check(result.RowsAffected, qt.Equals, int64(1)) var u2 dbmodel.Identity - result = db.Where("name = ?", "bob@external").First(&u2) + result = db.Where("name = ?", "bob@canonical.com").First(&u2) c.Assert(result.Error, qt.IsNil) c.Check(u2, qt.DeepEquals, u1) @@ -41,12 +41,12 @@ func TestIdentity(t *testing.T) { result = db.Save(&u2) c.Assert(result.Error, qt.IsNil) var u3 dbmodel.Identity - result = db.Where("name = ?", "bob@external").First(&u3) + result = db.Where("name = ?", "bob@canonical.com").First(&u3) c.Assert(result.Error, qt.IsNil) c.Check(u3, qt.DeepEquals, u2) u4 := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", DisplayName: "bob", } result = db.Create(&u4) @@ -57,10 +57,10 @@ func TestUserTag(t *testing.T) { c := qt.New(t) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } tag := u.Tag() - c.Check(tag.String(), qt.Equals, "user-bob@external") + c.Check(tag.String(), qt.Equals, "user-bob@canonical.com") var u2 dbmodel.Identity u2.SetTag(tag.(names.UserTag)) c.Check(u2, qt.DeepEquals, u) @@ -77,7 +77,7 @@ func TestIdentityCloudCredentials(t *testing.T) { c.Assert(result.Error, qt.IsNil) u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } result = db.Create(&u) c.Assert(result.Error, qt.IsNil) @@ -125,12 +125,12 @@ func TestIdentityToJujuUserInfo(t *testing.T) { Model: gorm.Model{ CreatedAt: time.Now(), }, - Name: "alice@external", + Name: "alice@canonical.com", DisplayName: "Alice", } ui := u.ToJujuUserInfo() c.Check(ui, qt.DeepEquals, jujuparams.UserInfo{ - Username: "alice@external", + Username: "alice@canonical.com", DisplayName: "Alice", Access: "", DateCreated: u.CreatedAt, @@ -142,7 +142,7 @@ func TestIdentityToJujuUserInfo(t *testing.T) { } ui = u.ToJujuUserInfo() c.Check(ui, qt.DeepEquals, jujuparams.UserInfo{ - Username: "alice@external", + Username: "alice@canonical.com", DisplayName: "Alice", Access: "", DateCreated: u.CreatedAt, diff --git a/internal/dbmodel/model.go b/internal/dbmodel/model.go index 740810dc1..74567d3d9 100644 --- a/internal/dbmodel/model.go +++ b/internal/dbmodel/model.go @@ -9,7 +9,7 @@ import ( "github.com/juju/juju/core/life" "github.com/juju/juju/core/status" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/version/v2" "github.com/canonical/jimm/internal/errors" diff --git a/internal/dbmodel/model_test.go b/internal/dbmodel/model_test.go index 59d740c65..c573c92a2 100644 --- a/internal/dbmodel/model_test.go +++ b/internal/dbmodel/model_test.go @@ -10,7 +10,7 @@ import ( qt "github.com/frankban/quicktest" "github.com/juju/juju/core/life" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "gorm.io/gorm" "github.com/canonical/jimm/internal/constants" @@ -241,7 +241,7 @@ func TestToJujuModel(t *testing.T) { Name: "test-model", UUID: "00000001-0000-0000-0000-0000-000000000001", Type: "iaas", - OwnerTag: "user-bob@external", + OwnerTag: "user-bob@canonical.com", }) } @@ -291,8 +291,8 @@ func TestToJujuModelSummary(t *testing.T) { DefaultSeries: "warty", CloudTag: "cloud-test-cloud", CloudRegion: "test-region", - CloudCredentialTag: "cloudcred-test-cloud_bob@external_test-cred", - OwnerTag: "user-bob@external", + CloudCredentialTag: "cloudcred-test-cloud_bob@canonical.com_test-cred", + OwnerTag: "user-bob@canonical.com", Life: life.Value(constants.ALIVE.String()), Status: jujuparams.EntityStatus{ Status: "available", @@ -318,7 +318,7 @@ func TestToJujuModelSummary(t *testing.T) { // that a model can be created. func initModelEnv(c *qt.C, db *gorm.DB) (dbmodel.Cloud, dbmodel.CloudCredential, dbmodel.Controller, dbmodel.Identity) { u := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(db.Create(&u).Error, qt.IsNil) @@ -366,16 +366,16 @@ func TestModelFromJujuModelInfo(t *testing.T) { DefaultSeries: "warty", CloudTag: "cloud-test-cloud", CloudRegion: "test-region", - CloudCredentialTag: "cloudcred-test-cloud_bob@external_test-cred", + CloudCredentialTag: "cloudcred-test-cloud_bob@canonical.com_test-cred", CloudCredentialValidity: nil, - OwnerTag: "user-bob@external", + OwnerTag: "user-bob@canonical.com", Life: life.Value(constants.ALIVE.String()), Status: jujuparams.EntityStatus{ Status: "available", Since: &now, }, Users: []jujuparams.ModelUserInfo{{ - UserName: "bob@external", + UserName: "bob@canonical.com", DisplayName: "Bobby The Tester", Access: "admin", }}, @@ -415,10 +415,10 @@ func TestModelFromJujuModelInfo(t *testing.T) { Name: "test-cred", CloudName: "test-cloud", Owner: dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", }, }, - OwnerIdentityName: "bob@external", + OwnerIdentityName: "bob@canonical.com", Type: "iaas", IsController: false, DefaultSeries: "warty", diff --git a/discharger.go b/internal/discharger/discharger.go similarity index 72% rename from discharger.go rename to internal/discharger/discharger.go index e3ce30274..cbb985359 100644 --- a/discharger.go +++ b/internal/discharger/discharger.go @@ -1,6 +1,6 @@ // Copyright 2023 Canonical Ltd. -package jimm +package discharger import ( "context" @@ -13,7 +13,7 @@ import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dbrootkeystore" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" jjmacaroon "github.com/juju/juju/core/macaroon" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" @@ -27,19 +27,26 @@ import ( var defaultDischargeExpiry = 15 * time.Minute -func newMacaroonDischarger(p Params, db *db.Database, ofgaClient *openfga.OFGAClient) (*macaroonDischarger, error) { +type MacaroonDischargerConfig struct { + PublicKey string + PrivateKey string + MacaroonExpiryDuration time.Duration + ControllerUUID string +} + +func NewMacaroonDischarger(cfg MacaroonDischargerConfig, db *db.Database, ofgaClient *openfga.OFGAClient) (*MacaroonDischarger, error) { var kp bakery.KeyPair - if p.PublicKey == "" || p.PrivateKey == "" { + if cfg.PublicKey == "" || cfg.PrivateKey == "" { generatedKP, err := bakery.GenerateKey() if err != nil { return nil, errors.E(err, "failed to generate a bakery keypair") } kp = *generatedKP } else { - if err := kp.Private.UnmarshalText([]byte(p.PrivateKey)); err != nil { + if err := kp.Private.UnmarshalText([]byte(cfg.PrivateKey)); err != nil { return nil, errors.E(err, "cannot unmarshal private key") } - if err := kp.Public.UnmarshalText([]byte(p.PublicKey)); err != nil { + if err := kp.Public.UnmarshalText([]byte(cfg.PublicKey)); err != nil { return nil, errors.E(err, "cannot unmarshal public key") } } @@ -51,27 +58,41 @@ func newMacaroonDischarger(p Params, db *db.Database, ofgaClient *openfga.OFGACl RootKeyStore: dbrootkeystore.NewRootKeys(100, nil).NewStore( db, dbrootkeystore.Policy{ - ExpiryDuration: p.MacaroonExpiryDuration, + ExpiryDuration: cfg.MacaroonExpiryDuration, }, ), Key: &kp, - Location: "jimm " + p.ControllerUUID, + Location: "jimm " + cfg.ControllerUUID, }, ) - return &macaroonDischarger{ + return &MacaroonDischarger{ ofgaClient: ofgaClient, bakery: b, kp: kp, }, nil } -type macaroonDischarger struct { +type MacaroonDischarger struct { ofgaClient *openfga.OFGAClient bakery *bakery.Bakery kp bakery.KeyPair } +// GetDischargerMux returns a mux that can handle macaroon bakery requests for the provided discharger. +func GetDischargerMux(MacaroonDischarger *MacaroonDischarger, rootPath string) *http.ServeMux { + discharger := httpbakery.NewDischarger( + httpbakery.DischargerParams{ + Key: &MacaroonDischarger.kp, + Checker: httpbakery.ThirdPartyCaveatCheckerFunc(MacaroonDischarger.CheckThirdPartyCaveat), + }, + ) + dischargeMux := http.NewServeMux() + discharger.AddMuxHandlers(dischargeMux, rootPath) + + return dischargeMux +} + // thirdPartyCaveatCheckerFunction returns a function that // checks third party caveats addressed to this service. // Caveat format is: @@ -82,7 +103,7 @@ type macaroonDischarger struct { // a declared caveat declaring offer uuid: // // declared offer-uuid -func (md *macaroonDischarger) checkThirdPartyCaveat(ctx context.Context, req *http.Request, cavInfo *bakery.ThirdPartyCaveatInfo, _ *httpbakery.DischargeToken) ([]checkers.Caveat, error) { +func (md *MacaroonDischarger) CheckThirdPartyCaveat(ctx context.Context, req *http.Request, cavInfo *bakery.ThirdPartyCaveatInfo, _ *httpbakery.DischargeToken) ([]checkers.Caveat, error) { caveatTokens := strings.Split(string(cavInfo.Condition), " ") if len(caveatTokens) != 3 { zapctx.Error(ctx, "caveat token length incorrect", zap.Int("length", len(caveatTokens))) diff --git a/internal/jimm/access.go b/internal/jimm/access.go index 0ca885462..da937474e 100644 --- a/internal/jimm/access.go +++ b/internal/jimm/access.go @@ -14,7 +14,7 @@ import ( "github.com/google/uuid" "github.com/juju/juju/core/crossmodel" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" @@ -50,13 +50,13 @@ var ( // [9] - Application offer name // [10] - Relation specifier (i.e., #member) // A complete matcher example would look like so with square-brackets denoting groups and paranthsis denoting index: - // (1)[controller](2)[-](3)[controller-1](4)[:](5)[alice@external-place](6)[/](7)[model-1](8)[.](9)[offer-1](10)[#relation-specifier]" + // (1)[controller](2)[-](3)[controller-1](4)[:](5)[alice@canonical.com-place](6)[/](7)[model-1](8)[.](9)[offer-1](10)[#relation-specifier]" // In the case of something like: user-alice@wonderland or group-alices-wonderland#member, it would look like so: // (1)[user](2)[-](3)[alices@wonderland] // (1)[group](2)[-](3)[alices-wonderland](10)[#member] // So if a group, user, UUID, controller name comes in, it will always be index 3 for them // and if a relation specifier is present, it will always be index 10 - jujuURIMatcher = regexp.MustCompile(`([a-zA-Z0-9]*)(\-|\z)([a-zA-Z0-9-@.]*)(\:|)([a-zA-Z0-9-@]*)(\/|)([a-zA-Z0-9-]*)(\.|)([a-zA-Z0-9-]*)([a-zA-Z#]*|\z)\z`) + jujuURIMatcher = regexp.MustCompile(`([a-zA-Z0-9]*)(\-|\z)([a-zA-Z0-9-@.]*)(\:|)([a-zA-Z0-9-@.]*)(\/|)([a-zA-Z0-9-]*)(\.|)([a-zA-Z0-9-]*)([a-zA-Z#]*|\z)\z`) ) // ToOfferAccessString maps relation to an application offer access string. @@ -228,6 +228,7 @@ func (auth *JWTGenerator) MakeLoginToken(ctx context.Context, req *jujuparams.Lo // Recreate the accessMapCache to prevent leaking permissions across multiple login requests. auth.accessMapCache = make(map[string]string) var authErr error + // TODO(CSS-7331) Refactor model proxy for new login methods auth.user, authErr = auth.authenticator.Authenticate(ctx, req) if authErr != nil { zapctx.Error(ctx, "authentication failed", zap.Error(authErr)) @@ -482,7 +483,7 @@ func (j *JIMM) ToJAASTag(ctx context.Context, tag *ofganames.Tag) (string, error } } -// resolveTag resolves JIMM tag [of any kind available] (i.e., controller-mycontroller:alex@external/mymodel.myoffer) +// resolveTag resolves JIMM tag [of any kind available] (i.e., controller-mycontroller:alex@canonical.com/mymodel.myoffer) // into a juju string tag (i.e., controller-). // // If the JIMM tag is aleady of juju string tag form, the transformation is left alone. diff --git a/internal/jimm/access_test.go b/internal/jimm/access_test.go index 95104dc06..42ebee6a5 100644 --- a/internal/jimm/access_test.go +++ b/internal/jimm/access_test.go @@ -14,7 +14,7 @@ import ( "github.com/google/uuid" "github.com/juju/juju/core/crossmodel" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/constants" "github.com/canonical/jimm/internal/db" @@ -204,7 +204,7 @@ func TestJWTGeneratorMakeLoginToken(t *testing.T) { }{{ about: "initial login, all is well", authenticator: &testAuthenticator{ - username: "eve@external", + username: "eve@canonical.com", }, database: &testDatabase{ ctl: dbmodel.Controller{ @@ -231,7 +231,7 @@ func TestJWTGeneratorMakeLoginToken(t *testing.T) { jwtService: &testJWTService{}, expectedJWTParams: jimmjwx.JWTParams{ Controller: ct.Id(), - User: names.NewUserTag("eve@external").String(), + User: names.NewUserTag("eve@canonical.com").String(), Access: map[string]string{ ct.String(): "superuser", mt.String(): "admin", @@ -241,14 +241,14 @@ func TestJWTGeneratorMakeLoginToken(t *testing.T) { }, { about: "authorization fails", authenticator: &testAuthenticator{ - username: "eve@external", + username: "eve@canonical.com", err: errors.E("a test error"), }, expectedError: "a test error", }, { about: "model access check fails", authenticator: &testAuthenticator{ - username: "eve@external", + username: "eve@canonical.com", }, accessChecker: &testAccessChecker{ modelAccessCheckErr: errors.E("a test error"), @@ -258,7 +258,7 @@ func TestJWTGeneratorMakeLoginToken(t *testing.T) { }, { about: "controller access check fails", authenticator: &testAuthenticator{ - username: "eve@external", + username: "eve@canonical.com", }, accessChecker: &testAccessChecker{ modelAccess: map[string]string{ @@ -270,7 +270,7 @@ func TestJWTGeneratorMakeLoginToken(t *testing.T) { }, { about: "get controller from db fails", authenticator: &testAuthenticator{ - username: "eve@external", + username: "eve@canonical.com", }, database: &testDatabase{ err: errors.E("a test error"), @@ -287,7 +287,7 @@ func TestJWTGeneratorMakeLoginToken(t *testing.T) { }, { about: "cloud access check fails", authenticator: &testAuthenticator{ - username: "eve@external", + username: "eve@canonical.com", }, database: &testDatabase{ ctl: dbmodel.Controller{ @@ -313,7 +313,7 @@ func TestJWTGeneratorMakeLoginToken(t *testing.T) { }, { about: "jwt service errors out", authenticator: &testAuthenticator{ - username: "eve@external", + username: "eve@canonical.com", }, database: &testDatabase{ ctl: dbmodel.Controller{ @@ -376,7 +376,7 @@ func TestJWTGeneratorMakeToken(t *testing.T) { jwtService: &testJWTService{}, expectedJWTParams: jimmjwx.JWTParams{ Controller: ct.Id(), - User: names.NewUserTag("eve@external").String(), + User: names.NewUserTag("eve@canonical.com").String(), Access: map[string]string{ ct.String(): "superuser", mt.String(): "admin", @@ -402,7 +402,7 @@ func TestJWTGeneratorMakeToken(t *testing.T) { }, expectedJWTParams: jimmjwx.JWTParams{ Controller: ct.Id(), - User: names.NewUserTag("eve@external").String(), + User: names.NewUserTag("eve@canonical.com").String(), Access: map[string]string{ ct.String(): "superuser", mt.String(): "admin", @@ -415,7 +415,7 @@ func TestJWTGeneratorMakeToken(t *testing.T) { for _, test := range tests { generator := jimm.NewJWTGenerator( &testAuthenticator{ - username: "eve@external", + username: "eve@canonical.com", }, &testDatabase{ ctl: dbmodel.Controller{ @@ -669,9 +669,9 @@ func TestResolveTagObjectMapsUsers(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - tag, err := jimm.ResolveTag(j.UUID, &j.Database, "user-alex@externally-werly#member") + tag, err := jimm.ResolveTag(j.UUID, &j.Database, "user-alex@canonical.comly-werly#member") c.Assert(err, qt.IsNil) - c.Assert(tag, qt.DeepEquals, ofganames.ConvertTagWithRelation(names.NewUserTag("alex@externally-werly"), ofganames.MemberRelation)) + c.Assert(tag, qt.DeepEquals, ofganames.ConvertTagWithRelation(names.NewUserTag("alex@canonical.comly-werly"), ofganames.MemberRelation)) } func TestResolveTupleObjectHandlesErrors(t *testing.T) { @@ -771,7 +771,7 @@ func createTestControllerEnvironment(ctx context.Context, c *qt.C, db db.Databas c.Assert(err, qt.IsNil) u := dbmodel.Identity{ - Name: petname.Generate(2, "-") + "@external", + Name: petname.Generate(2, "-") + "@canonical.com", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) diff --git a/internal/jimm/applicationoffer.go b/internal/jimm/applicationoffer.go index c1ebfd1bf..0dbf54311 100644 --- a/internal/jimm/applicationoffer.go +++ b/internal/jimm/applicationoffer.go @@ -11,7 +11,7 @@ import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/juju/core/crossmodel" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" @@ -276,7 +276,7 @@ func (j *JIMM) listApplicationOfferUsers(ctx context.Context, offer names.Applic userDetails := []jujuparams.OfferUserDetails{} for username, level := range users { // non-admin users should only see their own access level - // and access level of "everyone@external" - meaning the access + // and the access level of "everyone" - meaning the access // level everybody has. if accessLevel != string(jujuparams.OfferAdminAccess) && username != ofganames.EveryoneUser && username != user.Name { continue diff --git a/internal/jimm/applicationoffer_test.go b/internal/jimm/applicationoffer_test.go index ee481a3b4..bbe88f622 100644 --- a/internal/jimm/applicationoffer_test.go +++ b/internal/jimm/applicationoffer_test.go @@ -13,10 +13,10 @@ import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/juju/charm/v11" + "github.com/juju/charm/v12" "github.com/juju/juju/core/crossmodel" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "gopkg.in/macaroon.v2" "gorm.io/gorm" @@ -44,39 +44,39 @@ var initializeEnvironment = func(c *qt.C, ctx context.Context, db *db.Database, // Alice is a model admin, but not a superuser or offer admin. u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) u1 := dbmodel.Identity{ - Name: "eve@external", + Name: "eve@canonical.com", } c.Assert(db.DB.Create(&u1).Error, qt.IsNil) u2 := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(db.DB.Create(&u2).Error, qt.IsNil) u3 := dbmodel.Identity{ - Name: "fred@external", + Name: "fred@canonical.com", } c.Assert(db.DB.Create(&u3).Error, qt.IsNil) u4 := dbmodel.Identity{ - Name: "grant@external", + Name: "grant@canonical.com", } c.Assert(db.DB.Create(&u4).Error, qt.IsNil) // Jane is an offer admin, but not a superuser or model admin. u5 := dbmodel.Identity{ - Name: "jane@external", + Name: "jane@canonical.com", } c.Assert(db.DB.Create(&u5).Error, qt.IsNil) // Joe is a superuser, but not a model or offer admin. u6 := dbmodel.Identity{ - Name: "joe@external", + Name: "joe@canonical.com", } c.Assert(db.DB.Create(&u6).Error, qt.IsNil) @@ -569,17 +569,17 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { c.Assert(err, qt.IsNil) u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) u1 := dbmodel.Identity{ - Name: "eve@external", + Name: "eve@canonical.com", } c.Assert(db.DB.Create(&u1).Error, qt.IsNil) u2 := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(db.DB.Create(&u2).Error, qt.IsNil) @@ -692,13 +692,13 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { "key2": "value2", }, Users: []jujuparams.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "eve@external", + UserName: "eve@canonical.com", Access: "read", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "consume", }}, Spaces: []jujuparams.RemoteSpace{{ @@ -759,13 +759,13 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { "key2": "value2", }, Users: []jujuparams.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "consume", }, { - UserName: "eve@external", + UserName: "eve@canonical.com", Access: "read", }, { UserName: "everyone@external", @@ -814,7 +814,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { "key2": "value2", }, Users: []jujuparams.OfferUserDetails{{ - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "consume", }, { UserName: "everyone@external", @@ -923,10 +923,10 @@ func TestGetApplicationOffer(t *testing.T) { "key2": "value5", }, Users: []jujuparams.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: string(jujuparams.OfferAdminAccess), }, { - UserName: "eve@external", + UserName: "eve@canonical.com", Access: "read", }, { UserName: "admin", @@ -943,17 +943,17 @@ func TestGetApplicationOffer(t *testing.T) { c.Assert(err, qt.IsNil) u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) u1 := dbmodel.Identity{ - Name: "eve@external", + Name: "eve@canonical.com", } c.Assert(j.Database.DB.Create(&u1).Error, qt.IsNil) u2 := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&u2).Error, qt.IsNil) @@ -1078,10 +1078,10 @@ func TestGetApplicationOffer(t *testing.T) { "key2": "value5", }, Users: []jujuparams.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "eve@external", + UserName: "eve@canonical.com", Access: "read", }}, Spaces: []jujuparams.RemoteSpace{{ @@ -1129,7 +1129,7 @@ func TestGetApplicationOffer(t *testing.T) { "key2": "value5", }, Users: []jujuparams.OfferUserDetails{{ - UserName: "eve@external", + UserName: "eve@canonical.com", Access: "read", }}, Spaces: []jujuparams.RemoteSpace{{ @@ -1245,7 +1245,7 @@ func TestOffer(t *testing.T) { ctx := context.Background() u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1367,7 +1367,7 @@ func TestOffer(t *testing.T) { ctx := context.Background() u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1453,7 +1453,7 @@ func TestOffer(t *testing.T) { }, createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1488,7 +1488,7 @@ func TestOffer(t *testing.T) { ctx := context.Background() u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1577,12 +1577,12 @@ func TestOffer(t *testing.T) { ctx := context.Background() u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) u1 := dbmodel.Identity{ - Name: "eve@external", + Name: "eve@canonical.com", } c.Assert(db.DB.Create(&u1).Error, qt.IsNil) @@ -1670,7 +1670,7 @@ func TestOffer(t *testing.T) { ctx := context.Background() u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1758,7 +1758,7 @@ func TestOffer(t *testing.T) { ctx := context.Background() u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -1888,7 +1888,7 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { ctx := context.Background() u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(db.DB.Create(&u).Error, qt.IsNil) @@ -2573,23 +2573,23 @@ func TestFindApplicationOffers(t *testing.T) { }} } else { details.Users = []jujuparams.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "consume", }, { - UserName: "eve@external", + UserName: "eve@canonical.com", Access: "admin", }, { - UserName: "fred@external", + UserName: "fred@canonical.com", Access: "read", }, { - UserName: "jane@external", + UserName: "jane@canonical.com", Access: "admin", }, { // joe is jimm admin - UserName: "joe@external", + UserName: "joe@canonical.com", Access: "admin", }} } @@ -2627,7 +2627,7 @@ const listApplicationsTestEnv = `clouds: regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud controllers: @@ -2644,18 +2644,18 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: bob@external + owner: bob@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: admin - - user: charlie@external + - user: charlie@canonical.com access: read sla: level: unsupported @@ -2668,18 +2668,18 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: write - - user: charlie@external + - user: charlie@canonical.com access: read sla: level: unsupported @@ -2729,13 +2729,13 @@ func TestListApplicationOffers(t *testing.T) { "key2": "value2", }, Users: []jujuparams.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "eve@external", + UserName: "eve@canonical.com", Access: "read", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "consume", }}, Spaces: []jujuparams.RemoteSpace{{ @@ -2753,7 +2753,7 @@ func TestListApplicationOffers(t *testing.T) { Connections: []jujuparams.OfferConnection{{ SourceModelTag: "00000011-0000-0000-0000-000000000001", RelationId: 1, - Username: "charlie@external", + Username: "charlie@canonical.com", Endpoint: "an-endpoint", }}, }, { @@ -2774,13 +2774,13 @@ func TestListApplicationOffers(t *testing.T) { "key2": "value2", }, Users: []jujuparams.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "eve@external", + UserName: "eve@canonical.com", Access: "read", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "consume", }}, Spaces: []jujuparams.RemoteSpace{{ @@ -2798,7 +2798,7 @@ func TestListApplicationOffers(t *testing.T) { Connections: []jujuparams.OfferConnection{{ SourceModelTag: "00000011-0000-0000-0000-000000000002", RelationId: 2, - Username: "charlie@external", + Username: "charlie@canonical.com", Endpoint: "an-endpoint", }}, }}, nil @@ -2821,13 +2821,13 @@ func TestListApplicationOffers(t *testing.T) { "key2": "value2", }, Users: []jujuparams.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "eve@external", + UserName: "eve@canonical.com", Access: "read", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "consume", }}, Spaces: []jujuparams.RemoteSpace{{ @@ -2845,7 +2845,7 @@ func TestListApplicationOffers(t *testing.T) { Connections: []jujuparams.OfferConnection{{ SourceModelTag: "00000011-0000-0000-0000-000000000003", RelationId: 3, - Username: "charlie@external", + Username: "charlie@canonical.com", Endpoint: "an-endpoint", }}, }}, nil @@ -2857,46 +2857,46 @@ func TestListApplicationOffers(t *testing.T) { } env.PopulateDBAndPermissions(c, j.ResourceTag(), db, client) tuples := []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewApplicationOfferTag("00000012-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("eve@external")), + Object: ofganames.ConvertTag(names.NewUserTag("eve@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewApplicationOfferTag("00000012-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.ConsumerRelation, Target: ofganames.ConvertTag(names.NewApplicationOfferTag("00000012-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewApplicationOfferTag("00000012-0000-0000-0000-000000000002")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("eve@external")), + Object: ofganames.ConvertTag(names.NewUserTag("eve@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewApplicationOfferTag("00000012-0000-0000-0000-000000000002")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.ConsumerRelation, Target: ofganames.ConvertTag(names.NewApplicationOfferTag("00000012-0000-0000-0000-000000000002")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewApplicationOfferTag("00000012-0000-0000-0000-000000000003")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("eve@external")), + Object: ofganames.ConvertTag(names.NewUserTag("eve@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewApplicationOfferTag("00000012-0000-0000-0000-000000000003")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.ConsumerRelation, Target: ofganames.ConvertTag(names.NewApplicationOfferTag("00000012-0000-0000-0000-000000000003")), }} err = client.AddRelation(context.Background(), tuples...) c.Assert(err, qt.IsNil) - u := env.User("alice@external").DBObject(c, db) + u := env.User("alice@canonical.com").DBObject(c, db) _, err = j.ListApplicationOffers(ctx, openfga.NewUser(&u, client)) c.Assert(err, qt.ErrorMatches, `at least one filter must be specified`) @@ -2904,7 +2904,7 @@ func TestListApplicationOffers(t *testing.T) { c.Assert(err, qt.ErrorMatches, `application offer filter must specify a model name`) filters := []jujuparams.OfferFilter{{ - OwnerName: "bob@external", + OwnerName: "bob@canonical.com", ModelName: "model-1", }, { ModelName: "model-2", @@ -2936,13 +2936,13 @@ func TestListApplicationOffers(t *testing.T) { "key2": "value2", }, Users: []jujuparams.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "consume", }, { - UserName: "eve@external", + UserName: "eve@canonical.com", Access: "read", }}, Spaces: []jujuparams.RemoteSpace{{ @@ -2960,7 +2960,7 @@ func TestListApplicationOffers(t *testing.T) { Connections: []jujuparams.OfferConnection{{ SourceModelTag: "00000011-0000-0000-0000-000000000003", RelationId: 3, - Username: "charlie@external", + Username: "charlie@canonical.com", Endpoint: "an-endpoint", }}, }, { @@ -2981,13 +2981,13 @@ func TestListApplicationOffers(t *testing.T) { "key2": "value2", }, Users: []jujuparams.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "consume", }, { - UserName: "eve@external", + UserName: "eve@canonical.com", Access: "read", }}, Spaces: []jujuparams.RemoteSpace{{ @@ -3005,7 +3005,7 @@ func TestListApplicationOffers(t *testing.T) { Connections: []jujuparams.OfferConnection{{ SourceModelTag: "00000011-0000-0000-0000-000000000001", RelationId: 1, - Username: "charlie@external", + Username: "charlie@canonical.com", Endpoint: "an-endpoint", }}, }, { @@ -3026,13 +3026,13 @@ func TestListApplicationOffers(t *testing.T) { "key2": "value2", }, Users: []jujuparams.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "consume", }, { - UserName: "eve@external", + UserName: "eve@canonical.com", Access: "read", }}, Spaces: []jujuparams.RemoteSpace{{ @@ -3050,7 +3050,7 @@ func TestListApplicationOffers(t *testing.T) { Connections: []jujuparams.OfferConnection{{ SourceModelTag: "00000011-0000-0000-0000-000000000002", RelationId: 2, - Username: "charlie@external", + Username: "charlie@canonical.com", Endpoint: "an-endpoint", }}, }}) diff --git a/internal/jimm/audit_log.go b/internal/jimm/audit_log.go index 286a3fb9d..830f95bfb 100644 --- a/internal/jimm/audit_log.go +++ b/internal/jimm/audit_log.go @@ -9,7 +9,7 @@ import ( "github.com/juju/juju/rpc" "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" diff --git a/internal/jimm/cache.go b/internal/jimm/cache.go index 48d4c7a4d..a73d9aa4a 100644 --- a/internal/jimm/cache.go +++ b/internal/jimm/cache.go @@ -7,7 +7,7 @@ import ( "sync" "sync/atomic" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" "golang.org/x/sync/singleflight" diff --git a/internal/jimm/cache_test.go b/internal/jimm/cache_test.go index daaa21ce3..b5a172788 100644 --- a/internal/jimm/cache_test.go +++ b/internal/jimm/cache_test.go @@ -10,7 +10,7 @@ import ( "testing" qt "github.com/frankban/quicktest" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" diff --git a/internal/jimm/cloud.go b/internal/jimm/cloud.go index 435c13fad..633bfdb3d 100644 --- a/internal/jimm/cloud.go +++ b/internal/jimm/cloud.go @@ -8,7 +8,7 @@ import ( "strings" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" @@ -280,7 +280,7 @@ func (j *JIMM) AddCloudToController(ctx context.Context, user *openfga.User, con ) } - // TODO(Kian) CSS-6081 Give user access to the cloud here and potentially everyone@external. + // TODO(Kian) CSS-6081 Give user access to the cloud here and potentially everyone. return nil } diff --git a/internal/jimm/cloud_test.go b/internal/jimm/cloud_test.go index 426ae6794..13ae82241 100644 --- a/internal/jimm/cloud_test.go +++ b/internal/jimm/cloud_test.go @@ -10,7 +10,7 @@ import ( qt "github.com/frankban/quicktest" "github.com/google/uuid" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" @@ -42,22 +42,22 @@ func TestGetCloud(t *testing.T) { alice := openfga.NewUser( &dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", }, client, ) bob := openfga.NewUser( &dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", }, client, ) - charlie := openfga.NewUser(&dbmodel.Identity{Name: "charlie@external"}, client) + charlie := openfga.NewUser(&dbmodel.Identity{Name: "charlie@canonical.com"}, client) // daphne is a jimm administrator daphne := openfga.NewUser( &dbmodel.Identity{ - Name: "daphne@external", + Name: "daphne@canonical.com", }, client, ) @@ -169,19 +169,19 @@ func TestForEachCloud(t *testing.T) { c.Assert(err, qt.IsNil) alice := openfga.NewUser( - &dbmodel.Identity{Name: "alice@external"}, + &dbmodel.Identity{Name: "alice@canonical.com"}, client, ) bob := openfga.NewUser( - &dbmodel.Identity{Name: "bob@external"}, + &dbmodel.Identity{Name: "bob@canonical.com"}, client, ) charlie := openfga.NewUser( - &dbmodel.Identity{Name: "charlie@external"}, + &dbmodel.Identity{Name: "charlie@canonical.com"}, client, ) daphne := openfga.NewUser( - &dbmodel.Identity{Name: "daphne@external"}, + &dbmodel.Identity{Name: "daphne@canonical.com"}, client, ) daphne.JimmAdmin = true @@ -336,14 +336,14 @@ const addHostedCloudTestEnv = `clouds: regions: - name: test-region users: - - user: alice@external + - user: alice@canonical.com access: admin - name: private-cloud2 type: test-provider3 regions: - name: test-region-2 users: - - user: bob@external + - user: bob@canonical.com access: admin - name: existing-cloud type: kubernetes @@ -351,7 +351,7 @@ const addHostedCloudTestEnv = `clouds: regions: - name: default users: - - user: alice@external + - user: alice@canonical.com access: admin controllers: - name: test-controller @@ -366,9 +366,9 @@ controllers: region: default priority: 1 users: -- username: alice@external +- username: alice@canonical.com controller-access: superuser -- username: bob@external +- username: bob@canonical.com controller-access: login ` @@ -409,7 +409,7 @@ var addHostedCloudTests = []struct { } return nil }, - username: "bob@external", + username: "bob@canonical.com", cloudName: "new-cloud", cloud: jujuparams.Cloud{ Type: "kubernetes", @@ -445,7 +445,7 @@ var addHostedCloudTests = []struct { }, }, { name: "CloudWithReservedName", - username: "bob@external", + username: "bob@canonical.com", cloudName: "aws", cloud: jujuparams.Cloud{ Type: "kubernetes", @@ -459,7 +459,7 @@ var addHostedCloudTests = []struct { expectErrorCode: errors.CodeAlreadyExists, }, { name: "ExistingCloud", - username: "bob@external", + username: "bob@canonical.com", cloudName: "existing-cloud", cloud: jujuparams.Cloud{ Type: "kubernetes", @@ -473,7 +473,7 @@ var addHostedCloudTests = []struct { expectErrorCode: errors.CodeAlreadyExists, }, { name: "InvalidCloudType", - username: "bob@external", + username: "bob@canonical.com", cloudName: "new-cloud", cloud: jujuparams.Cloud{ Type: "ec2", @@ -487,7 +487,7 @@ var addHostedCloudTests = []struct { expectErrorCode: errors.CodeIncompatibleClouds, }, { name: "HostCloudRegionNotFound", - username: "bob@external", + username: "bob@canonical.com", cloudName: "new-cloud", cloud: jujuparams.Cloud{ Type: "kubernetes", @@ -501,7 +501,7 @@ var addHostedCloudTests = []struct { expectErrorCode: errors.CodeNotFound, }, { name: "InvalidHostCloudRegion", - username: "bob@external", + username: "bob@canonical.com", cloudName: "new-cloud", cloud: jujuparams.Cloud{ Type: "kubernetes", @@ -515,7 +515,7 @@ var addHostedCloudTests = []struct { expectErrorCode: errors.CodeBadRequest, }, { name: "UserHasNoCloudAccess", - username: "bob@external", + username: "bob@canonical.com", cloudName: "new-cloud", cloud: jujuparams.Cloud{ Type: "kubernetes", @@ -529,7 +529,7 @@ var addHostedCloudTests = []struct { expectErrorCode: errors.CodeUnauthorized, }, { name: "HostCloudIsHosted", - username: "alice@external", + username: "alice@canonical.com", cloudName: "new-cloud", cloud: jujuparams.Cloud{ Type: "kubernetes", @@ -544,7 +544,7 @@ var addHostedCloudTests = []struct { }, { name: "DialError", dialError: errors.E("dial error"), - username: "alice@external", + username: "alice@canonical.com", cloudName: "new-cloud", cloud: jujuparams.Cloud{ Type: "kubernetes", @@ -560,7 +560,7 @@ var addHostedCloudTests = []struct { addCloud: func(context.Context, names.CloudTag, jujuparams.Cloud, bool) error { return errors.E("addcloud error") }, - username: "alice@external", + username: "alice@canonical.com", cloudName: "new-cloud", cloud: jujuparams.Cloud{ Type: "kubernetes", @@ -672,7 +672,7 @@ var addHostedCloudToControllerTests = []struct { } return nil }, - username: "alice@external", + username: "alice@canonical.com", controllerName: "test-controller", cloudName: "new-cloud", cloud: jujuparams.Cloud{ @@ -731,7 +731,7 @@ var addHostedCloudToControllerTests = []struct { } return nil }, - username: "alice@external", + username: "alice@canonical.com", controllerName: "no-such-controller", cloudName: "new-cloud", cloud: jujuparams.Cloud{ @@ -746,7 +746,7 @@ var addHostedCloudToControllerTests = []struct { expectErrorCode: errors.CodeNotFound, }, { name: "CloudWithReservedName", - username: "alice@external", + username: "alice@canonical.com", controllerName: "test-controller", cloudName: "aws", cloud: jujuparams.Cloud{ @@ -761,7 +761,7 @@ var addHostedCloudToControllerTests = []struct { expectErrorCode: errors.CodeAlreadyExists, }, { name: "HostCloudRegionNotFound", - username: "alice@external", + username: "alice@canonical.com", controllerName: "test-controller", cloudName: "new-cloud", cloud: jujuparams.Cloud{ @@ -776,7 +776,7 @@ var addHostedCloudToControllerTests = []struct { expectErrorCode: errors.CodeIncompatibleClouds, }, { name: "InvalidHostCloudRegion", - username: "alice@external", + username: "alice@canonical.com", controllerName: "test-controller", cloudName: "new-cloud", cloud: jujuparams.Cloud{ @@ -791,7 +791,7 @@ var addHostedCloudToControllerTests = []struct { expectErrorCode: errors.CodeIncompatibleClouds, }, { name: "UserHasNoCloudAccess", - username: "alice@external", + username: "alice@canonical.com", controllerName: "test-controller", cloudName: "new-cloud", cloud: jujuparams.Cloud{ @@ -806,7 +806,7 @@ var addHostedCloudToControllerTests = []struct { expectErrorCode: errors.CodeIncompatibleClouds, }, { name: "HostCloudIsHosted", - username: "alice@external", + username: "alice@canonical.com", controllerName: "test-controller", cloudName: "new-cloud", cloud: jujuparams.Cloud{ @@ -822,7 +822,7 @@ var addHostedCloudToControllerTests = []struct { }, { name: "DialError", dialError: errors.E("dial error"), - username: "alice@external", + username: "alice@canonical.com", controllerName: "test-controller", cloudName: "new-cloud", cloud: jujuparams.Cloud{ @@ -839,7 +839,7 @@ var addHostedCloudToControllerTests = []struct { addCloud: func(context.Context, names.CloudTag, jujuparams.Cloud, bool) error { return errors.E("addcloud error") }, - username: "alice@external", + username: "alice@canonical.com", controllerName: "test-controller", cloudName: "new-cloud", cloud: jujuparams.Cloud{ @@ -928,7 +928,7 @@ const grantCloudAccessTestEnv = `clouds: - name: default - name: region2 users: - - user: alice@external + - user: alice@canonical.com access: admin controllers: - name: controller-1 @@ -960,50 +960,50 @@ var grantCloudAccessTests = []struct { expectErrorCode errors.Code }{{ name: "CloudNotFound", - username: "alice@external", + username: "alice@canonical.com", cloud: "test2", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "add-model", expectError: `cloud "test2" not found`, expectErrorCode: errors.CodeNotFound, }, { name: "Admin grants admin access", env: grantCloudAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "admin", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }}, }, { name: "Admin grants add-model access", env: grantCloudAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "add-model", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.CanAddModelRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }}, }, { name: "UserNotAuthorized", env: grantCloudAccessTestEnv, - username: "charlie@external", + username: "charlie@canonical.com", cloud: "test", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "add-model", expectError: `unauthorized`, expectErrorCode: errors.CodeUnauthorized, @@ -1011,17 +1011,17 @@ var grantCloudAccessTests = []struct { name: "DialError", env: grantCloudAccessTestEnv, dialError: errors.E("test dial error"), - username: "alice@external", + username: "alice@canonical.com", cloud: "test", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "add-model", expectError: `test dial error`, }, { name: "unknown access", env: grantCloudAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "some-unknown-access", expectError: `failed to recognize given access: "some-unknown-access"`, }} @@ -1081,7 +1081,7 @@ const revokeCloudAccessTestEnv = `clouds: regions: - name: test-cloud-region users: - - user: daphne@external + - user: daphne@canonical.com access: admin - name: test type: kubernetes @@ -1089,11 +1089,11 @@ const revokeCloudAccessTestEnv = `clouds: regions: - name: default users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: admin - - user: charlie@external + - user: charlie@canonical.com access: add-model controllers: - name: controller-1 @@ -1124,185 +1124,185 @@ var revokeCloudAccessTests = []struct { expectErrorCode errors.Code }{{ name: "CloudNotFound", - username: "alice@external", + username: "alice@canonical.com", cloud: "test2", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "admin", expectError: `cloud "test2" not found`, expectErrorCode: errors.CodeNotFound, }, { name: "Admin revokes 'admin' from another admin", env: revokeCloudAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "admin", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.CanAddModelRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }}, }, { name: "Admin revokes 'add-model' from another admin", env: revokeCloudAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "add-model", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.CanAddModelRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }}, }, { name: "Admin revokes 'add-model' from a user with 'add-model' access", env: revokeCloudAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", - targetUsername: "charlie@external", + targetUsername: "charlie@canonical.com", access: "add-model", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.CanAddModelRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }}, }, { name: "Admin revokes 'add-model' from a user with no access", env: revokeCloudAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", - targetUsername: "daphne@external", + targetUsername: "daphne@canonical.com", access: "add-model", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.CanAddModelRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }}, }, { name: "Admin revokes 'admin' from a user with no access", env: revokeCloudAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", - targetUsername: "daphne@external", + targetUsername: "daphne@canonical.com", access: "admin", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.CanAddModelRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }}, }, { name: "Admin revokes 'add-model' access from a user who has separate tuples for all accesses (add-model/admin)", env: revokeCloudAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", - targetUsername: "charlie@external", + targetUsername: "charlie@canonical.com", access: "add-model", extraInitialTuples: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, // No need to add the 'add-model' relation, because it's already there due to the environment setup. }, expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.CanAddModelRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }}, }, { name: "Admin revokes 'admin' access from a user who has separate tuples for all accesses (add-model/admin)", env: revokeCloudAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", - targetUsername: "charlie@external", + targetUsername: "charlie@canonical.com", access: "admin", extraInitialTuples: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, // No need to add the 'add-model' relation, because it's already there due to the environment setup. }, expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.CanAddModelRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewCloudTag("test")), }}, }, { name: "UserNotAuthorized", env: revokeCloudAccessTestEnv, - username: "charlie@external", + username: "charlie@canonical.com", cloud: "test", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "add-model", expectError: `unauthorized`, expectErrorCode: errors.CodeUnauthorized, @@ -1310,17 +1310,17 @@ var revokeCloudAccessTests = []struct { name: "DialError", env: revokeCloudAccessTestEnv, dialError: errors.E("test dial error"), - username: "alice@external", + username: "alice@canonical.com", cloud: "test", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "add-model", expectError: `test dial error`, }, { name: "unknown access", env: revokeCloudAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "some-unknown-access", expectError: `failed to recognize given access: "some-unknown-access"`, }} @@ -1408,9 +1408,9 @@ const removeCloudTestEnv = `clouds: regions: - name: default users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: add-model controllers: - name: controller-1 @@ -1437,7 +1437,7 @@ var removeCloudTests = []struct { expectErrorCode errors.Code }{{ name: "CloudNotFound", - username: "alice@external", + username: "alice@canonical.com", cloud: "test2", expectError: `cloud "test2" not found`, expectErrorCode: errors.CodeNotFound, @@ -1450,12 +1450,12 @@ var removeCloudTests = []struct { } return nil }, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", }, { name: "UserNotAuthorized", env: removeCloudTestEnv, - username: "bob@external", + username: "bob@canonical.com", cloud: "test", expectError: `unauthorized`, expectErrorCode: errors.CodeUnauthorized, @@ -1463,7 +1463,7 @@ var removeCloudTests = []struct { name: "DialError", env: removeCloudTestEnv, dialError: errors.E("test dial error"), - username: "alice@external", + username: "alice@canonical.com", cloud: "test", expectError: `test dial error`, }, { @@ -1472,7 +1472,7 @@ var removeCloudTests = []struct { removeCloud: func(_ context.Context, mt names.CloudTag) error { return errors.E("test error") }, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", expectError: `test error`, }} @@ -1539,9 +1539,9 @@ const updateCloudTestEnv = `clouds: regions: - name: default users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: admin controllers: - name: controller-1 @@ -1556,7 +1556,7 @@ controllers: region: default priority: 1 users: -- username: alice@external +- username: alice@canonical.com controller-access: superuser ` @@ -1573,12 +1573,12 @@ var updateCloudTests = []struct { expectCloud dbmodel.Cloud }{{ name: "CloudNotFound", - username: "alice@external", + username: "alice@canonical.com", cloud: "test2", expectError: `cloud "test2" not found`, expectErrorCode: errors.CodeNotFound, }, /* NOTE (alesstimec) Need to figure out what makes test-cloud - a public cloud giving alice@external the right + a public cloud giving alice@canonical.com the right to update it. { name: "SuccessPublicCloud", @@ -1589,7 +1589,7 @@ var updateCloudTests = []struct { } return nil }, - username: "alice@external", + username: "alice@canonical.com", cloud: "test-cloud", update: jujuparams.Cloud{ Type: "test-provider", @@ -1656,7 +1656,7 @@ var updateCloudTests = []struct { } return nil }, - username: "bob@external", + username: "bob@canonical.com", cloud: "test", update: jujuparams.Cloud{ Type: "kubernetes", @@ -1693,7 +1693,7 @@ var updateCloudTests = []struct { }, { name: "UserNotAuthorized", env: updateCloudTestEnv, - username: "bob@external", + username: "bob@canonical.com", cloud: "test-cloud", expectError: `unauthorized`, expectErrorCode: errors.CodeUnauthorized, @@ -1701,7 +1701,7 @@ var updateCloudTests = []struct { name: "DialError", env: updateCloudTestEnv, dialError: errors.E("test dial error"), - username: "alice@external", + username: "alice@canonical.com", cloud: "test", expectError: `test dial error`, }, { @@ -1710,7 +1710,7 @@ var updateCloudTests = []struct { updateCloud: func(context.Context, names.CloudTag, jujuparams.Cloud) error { return errors.E("test error") }, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", expectError: `test error`, }} @@ -1780,9 +1780,9 @@ const removeCloudFromControllerTestEnv = `clouds: regions: - name: default users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: add-model - name: test type: kubernetes @@ -1790,9 +1790,9 @@ const removeCloudFromControllerTestEnv = `clouds: regions: - name: default users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: add-model controllers: - name: controller-1 @@ -1835,7 +1835,7 @@ var removeCloudFromControllerTests = []struct { assertSuccess func(c *qt.C, j *jimm.JIMM) }{{ name: "CloudNotFound", - username: "alice@external", + username: "alice@canonical.com", cloud: "test2", controllerName: "controller-2", expectError: `cloud "test2" not found`, @@ -1849,7 +1849,7 @@ var removeCloudFromControllerTests = []struct { } return nil }, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", controllerName: "controller-2", assertSuccess: func(c *qt.C, j *jimm.JIMM) { @@ -1873,7 +1873,7 @@ var removeCloudFromControllerTests = []struct { } return nil }, - username: "alice@external", + username: "alice@canonical.com", cloud: "test-cloud-2", controllerName: "controller-2", assertSuccess: func(c *qt.C, j *jimm.JIMM) { @@ -1886,7 +1886,7 @@ var removeCloudFromControllerTests = []struct { }, { name: "UserNotAutfhorized", env: removeCloudFromControllerTestEnv, - username: "bob@external", + username: "bob@canonical.com", cloud: "test", controllerName: "controller-2", expectError: `unauthorized`, @@ -1895,7 +1895,7 @@ var removeCloudFromControllerTests = []struct { name: "DialError", env: removeCloudFromControllerTestEnv, dialError: errors.E("test dial error"), - username: "alice@external", + username: "alice@canonical.com", cloud: "test", controllerName: "controller-2", expectError: `test dial error`, @@ -1905,7 +1905,7 @@ var removeCloudFromControllerTests = []struct { removeCloud: func(_ context.Context, mt names.CloudTag) error { return errors.E("test error") }, - username: "alice@external", + username: "alice@canonical.com", cloud: "test", controllerName: "controller-2", expectError: `test error`, diff --git a/internal/jimm/cloudcredential.go b/internal/jimm/cloudcredential.go index eb122c671..99ad8d353 100644 --- a/internal/jimm/cloudcredential.go +++ b/internal/jimm/cloudcredential.go @@ -10,7 +10,7 @@ import ( "sync" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" diff --git a/internal/jimm/cloudcredential_test.go b/internal/jimm/cloudcredential_test.go index 164e22dc4..717a0fc04 100644 --- a/internal/jimm/cloudcredential_test.go +++ b/internal/jimm/cloudcredential_test.go @@ -14,7 +14,7 @@ import ( "github.com/juju/juju/core/life" "github.com/juju/juju/core/status" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/lestrrat-go/jwx/v2/jwk" "github.com/canonical/jimm/internal/db" @@ -45,7 +45,7 @@ func TestUpdateCloudCredential(t *testing.T) { jimmAdmin: true, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -112,12 +112,12 @@ func TestUpdateCloudCredential(t *testing.T) { Owner: names.NewUserTag(u.Name), Cloud: names.NewCloudTag(cloud.Name), CloudRegion: "test-region-1", - CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), + CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1"), }) c.Assert(err, qt.Equals, nil) arg := jimm.UpdateCloudCredentialArgs{ - CredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), + CredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1"), Credential: jujuparams.CloudCredential{ Attributes: map[string]string{ "key1": "value1", @@ -159,7 +159,7 @@ func TestUpdateCloudCredential(t *testing.T) { updateCredentialErrors: []error{nil, errors.E("test error")}, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -227,12 +227,12 @@ func TestUpdateCloudCredential(t *testing.T) { Owner: names.NewUserTag(u.Name), Cloud: names.NewCloudTag(cloud.Name), CloudRegion: "test-region-1", - CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), + CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1"), }) c.Assert(err, qt.Equals, nil) arg := jimm.UpdateCloudCredentialArgs{ - CredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), + CredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1"), Credential: jujuparams.CloudCredential{ Attributes: map[string]string{ "key1": "value1", @@ -250,7 +250,7 @@ func TestUpdateCloudCredential(t *testing.T) { updateCredentialErrors: []error{nil}, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -313,12 +313,12 @@ func TestUpdateCloudCredential(t *testing.T) { Owner: names.NewUserTag(u.Name), Cloud: names.NewCloudTag(cloud.Name), CloudRegion: "test-region-1", - CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), + CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1"), }) c.Assert(err, qt.Equals, nil) arg := jimm.UpdateCloudCredentialArgs{ - CredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), + CredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1"), Credential: jujuparams.CloudCredential{ Attributes: map[string]string{ "key1": "value1", @@ -334,7 +334,7 @@ func TestUpdateCloudCredential(t *testing.T) { jimmAdmin: true, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -345,7 +345,7 @@ func TestUpdateCloudCredential(t *testing.T) { c.Assert(err, qt.IsNil) eve := dbmodel.Identity{ - Name: "eve@external", + Name: "eve@canonical.com", } c.Assert(j.Database.DB.Create(&eve).Error, qt.IsNil) @@ -404,15 +404,15 @@ func TestUpdateCloudCredential(t *testing.T) { mi, err := j.AddModel(context.Background(), alice, &jimm.ModelCreateArgs{ Name: "test-model", - Owner: names.NewUserTag("eve@external"), + Owner: names.NewUserTag("eve@canonical.com"), Cloud: names.NewCloudTag(cloud.Name), CloudRegion: "test-region-1", - CloudCredential: names.NewCloudCredentialTag("test-cloud/eve@external/test-credential-1"), + CloudCredential: names.NewCloudCredentialTag("test-cloud/eve@canonical.com/test-credential-1"), }) c.Assert(err, qt.Equals, nil) arg := jimm.UpdateCloudCredentialArgs{ - CredentialTag: names.NewCloudCredentialTag("test-cloud/eve@external/test-credential-1"), + CredentialTag: names.NewCloudCredentialTag("test-cloud/eve@canonical.com/test-credential-1"), Credential: jujuparams.CloudCredential{ Attributes: map[string]string{ "key1": "value1", @@ -458,7 +458,7 @@ func TestUpdateCloudCredential(t *testing.T) { jimmAdmin: true, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -526,12 +526,12 @@ func TestUpdateCloudCredential(t *testing.T) { Owner: names.NewUserTag(u.Name), Cloud: names.NewCloudTag(cloud.Name), CloudRegion: "test-region-1", - CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), + CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1"), }) c.Assert(err, qt.Equals, nil) arg := jimm.UpdateCloudCredentialArgs{ - CredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), + CredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1"), Credential: jujuparams.CloudCredential{ Attributes: map[string]string{ "key1": "value1", @@ -571,7 +571,7 @@ func TestUpdateCloudCredential(t *testing.T) { jimmAdmin: true, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -638,12 +638,12 @@ func TestUpdateCloudCredential(t *testing.T) { Owner: names.NewUserTag(u.Name), Cloud: names.NewCloudTag(cloud.Name), CloudRegion: "test-region-1", - CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), + CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1"), }) c.Assert(err, qt.Equals, nil) arg := jimm.UpdateCloudCredentialArgs{ - CredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), + CredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1"), Credential: jujuparams.CloudCredential{ Attributes: map[string]string{ "key1": "value1", @@ -751,7 +751,7 @@ func TestUpdateCloudCredential(t *testing.T) { } mi.Life = life.Alive mi.Users = []jujuparams.ModelUserInfo{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: jujuparams.ModelAdminAccess, }, { // "bob" is a local user @@ -832,7 +832,7 @@ func TestUpdateCloudCredentialForUnknownUser(t *testing.T) { regions: - name: default users: -- username: alice@external +- username: alice@canonical.com controller-access: superuser `) j := &jimm.JIMM{ @@ -849,11 +849,11 @@ users: err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) env.PopulateDBAndPermissions(c, j.ResourceTag(), j.Database, client) - u := env.User("alice@external").DBObject(c, j.Database) + u := env.User("alice@canonical.com").DBObject(c, j.Database) user := openfga.NewUser(&u, client) user.JimmAdmin = true _, err = j.UpdateCloudCredential(ctx, user, jimm.UpdateCloudCredentialArgs{ - CredentialTag: names.NewCloudCredentialTag("test-cloud/bob@external/test"), + CredentialTag: names.NewCloudCredentialTag("test-cloud/bob@canonical.com/test"), Credential: jujuparams.CloudCredential{ AuthType: "empty", }, @@ -877,7 +877,7 @@ func TestRevokeCloudCredential(t *testing.T) { about: "credential revoked", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -940,7 +940,7 @@ func TestRevokeCloudCredential(t *testing.T) { Type: "test-provider", } - tag := names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1") + tag := names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1") return &u, tag, "" }, }, { @@ -951,7 +951,7 @@ func TestRevokeCloudCredential(t *testing.T) { }}, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1014,14 +1014,14 @@ func TestRevokeCloudCredential(t *testing.T) { Type: "test-provider", } - tag := names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1") + tag := names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1") return &u, tag, "" }, }, { about: "credential still used by a model", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1084,11 +1084,11 @@ func TestRevokeCloudCredential(t *testing.T) { Owner: names.NewUserTag(u.Name), Cloud: names.NewCloudTag(cloud.Name), CloudRegion: "test-region-1", - CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), + CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1"), }) c.Assert(err, qt.Equals, nil) - tag := names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1") + tag := names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1") return &u, tag, `cloud credential still used by 1 model\(s\)` }, @@ -1096,7 +1096,7 @@ func TestRevokeCloudCredential(t *testing.T) { about: "user not owner of credentials - unauthorizer error", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1105,7 +1105,7 @@ func TestRevokeCloudCredential(t *testing.T) { err := alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) - tag := names.NewCloudCredentialTag("test-cloud/eve@external/test-credential-1") + tag := names.NewCloudCredentialTag("test-cloud/eve@canonical.com/test-credential-1") return &u, tag, "unauthorized" }, @@ -1114,7 +1114,7 @@ func TestRevokeCloudCredential(t *testing.T) { revokeCredentialErrors: []error{errors.E("test error")}, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1172,7 +1172,7 @@ func TestRevokeCloudCredential(t *testing.T) { err = j.Database.SetCloudCredential(context.Background(), &cred) c.Assert(err, qt.Equals, nil) - tag := names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1") + tag := names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1") return &u, tag, "test error" }, @@ -1214,7 +1214,7 @@ func TestRevokeCloudCredential(t *testing.T) { } mi.Life = life.Alive mi.Users = []jujuparams.ModelUserInfo{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: jujuparams.ModelAdminAccess, }, { // "bob" is a local user @@ -1286,7 +1286,7 @@ func TestGetCloudCredential(t *testing.T) { about: "all ok", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1348,7 +1348,7 @@ func TestGetCloudCredential(t *testing.T) { err = j.Database.SetCloudCredential(context.Background(), &cred) c.Assert(err, qt.Equals, nil) - tag := names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1") + tag := names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1") return &u, tag, cred, "" }, @@ -1356,7 +1356,7 @@ func TestGetCloudCredential(t *testing.T) { about: "credential not found", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, dbmodel.CloudCredential, string) { u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) @@ -1365,9 +1365,9 @@ func TestGetCloudCredential(t *testing.T) { err := alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) - tag := names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1") + tag := names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1") - return &u, tag, dbmodel.CloudCredential{}, `cloudcredential "test-cloud/alice@external/test-credential-1" not found` + return &u, tag, dbmodel.CloudCredential{}, `cloudcredential "test-cloud/alice@canonical.com/test-credential-1" not found` }, }} for _, test := range tests { @@ -1410,29 +1410,29 @@ const forEachUserCloudCredentialEnv = `clouds: cloud-credentials: - name: cred-1 cloud: cloud-1 - owner: alice@external + owner: alice@canonical.com attributes: k1: v1 k2: v2 - name: cred-2 cloud: cloud-1 - owner: bob@external + owner: bob@canonical.com attributes: k1: v1 k2: v2 - name: cred-3 cloud: cloud-2 - owner: alice@external + owner: alice@canonical.com - name: cred-4 cloud: cloud-2 - owner: bob@external + owner: bob@canonical.com - name: cred-5 cloud: cloud-1 - owner: alice@external + owner: alice@canonical.com users: -- username: alice@external +- username: alice@canonical.com controller-access: superuser -- username: bob@external +- username: bob@canonical.com ` var forEachUserCloudCredentialTests = []struct { @@ -1447,24 +1447,24 @@ var forEachUserCloudCredentialTests = []struct { }{{ name: "UserCredentialsWithCloud", env: forEachUserCloudCredentialEnv, - username: "alice@external", + username: "alice@canonical.com", cloudTag: names.NewCloudTag("cloud-1"), expectCredentials: []string{ - names.NewCloudCredentialTag("cloud-1/alice@external/cred-1").String(), - names.NewCloudCredentialTag("cloud-1/alice@external/cred-5").String(), + names.NewCloudCredentialTag("cloud-1/alice@canonical.com/cred-1").String(), + names.NewCloudCredentialTag("cloud-1/alice@canonical.com/cred-5").String(), }, }, { name: "UserCredentialsWithoutCloud", env: forEachUserCloudCredentialEnv, - username: "bob@external", + username: "bob@canonical.com", expectCredentials: []string{ - names.NewCloudCredentialTag("cloud-1/bob@external/cred-2").String(), - names.NewCloudCredentialTag("cloud-2/bob@external/cred-4").String(), + names.NewCloudCredentialTag("cloud-1/bob@canonical.com/cred-2").String(), + names.NewCloudCredentialTag("cloud-2/bob@canonical.com/cred-4").String(), }, }, { name: "IterationError", env: forEachUserCloudCredentialEnv, - username: "alice@external", + username: "alice@canonical.com", f: func(*dbmodel.CloudCredential) error { return errors.E("test error", errors.Code("test code")) }, @@ -1530,7 +1530,7 @@ const getCloudCredentialAttributesEnv = `clouds: cloud-credentials: - name: cred-1 cloud: test-cloud - owner: bob@external + owner: bob@canonical.com auth-type: oauth2 attributes: client-email: bob@example.com @@ -1538,9 +1538,9 @@ cloud-credentials: private-key: super-secret project-id: 5678 users: -- username: alice@external +- username: alice@canonical.com controller-access: superuser -- username: bob@external +- username: bob@canonical.com ` var getCloudCredentialAttributesTests = []struct { @@ -1554,7 +1554,7 @@ var getCloudCredentialAttributesTests = []struct { expectErrorCode errors.Code }{{ name: "OwnerNoHidden", - username: "bob@external", + username: "bob@canonical.com", jimmAdmin: true, expectAttributes: map[string]string{ "client-email": "bob@example.com", @@ -1564,7 +1564,7 @@ var getCloudCredentialAttributesTests = []struct { expectRedacted: []string{"private-key"}, }, { name: "OwnerWithHidden", - username: "bob@external", + username: "bob@canonical.com", hidden: true, expectAttributes: map[string]string{ "client-email": "bob@example.com", @@ -1574,7 +1574,7 @@ var getCloudCredentialAttributesTests = []struct { }, }, { name: "SuperUserNoHidden", - username: "alice@external", + username: "alice@canonical.com", jimmAdmin: true, expectAttributes: map[string]string{ "client-email": "bob@example.com", @@ -1584,14 +1584,14 @@ var getCloudCredentialAttributesTests = []struct { expectRedacted: []string{"private-key"}, }, { name: "SuperUserWithHiddenUnauthorized", - username: "alice@external", + username: "alice@canonical.com", hidden: true, jimmAdmin: true, expectError: `unauthorized`, expectErrorCode: errors.CodeUnauthorized, }, { name: "OtherUserUnauthorized", - username: "charlie@external", + username: "charlie@canonical.com", expectError: `unauthorized`, expectErrorCode: errors.CodeUnauthorized, }} @@ -1620,9 +1620,9 @@ func TestGetCloudCredentialAttributes(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) env.PopulateDBAndPermissions(c, j.ResourceTag(), j.Database, client) - u := env.User("bob@external").DBObject(c, j.Database) + u := env.User("bob@canonical.com").DBObject(c, j.Database) userBob := openfga.NewUser(&u, client) - cred, err := j.GetCloudCredential(ctx, userBob, names.NewCloudCredentialTag("test-cloud/bob@external/cred-1")) + cred, err := j.GetCloudCredential(ctx, userBob, names.NewCloudCredentialTag("test-cloud/bob@canonical.com/cred-1")) c.Assert(err, qt.IsNil) u = env.User(test.username).DBObject(c, j.Database) @@ -1674,10 +1674,10 @@ func TestCloudCredentialAttributeStore(t *testing.T) { `) env.PopulateDBAndPermissions(c, j.ResourceTag(), j.Database, client) - u := env.User("alice@external").DBObject(c, j.Database) + u := env.User("alice@canonical.com").DBObject(c, j.Database) user := openfga.NewUser(&u, client) args := jimm.UpdateCloudCredentialArgs{ - CredentialTag: names.NewCloudCredentialTag("test/alice@external/cred-1"), + CredentialTag: names.NewCloudCredentialTag("test/alice@canonical.com/cred-1"), Credential: jujuparams.CloudCredential{ AuthType: "userpass", Attributes: map[string]string{ @@ -1690,14 +1690,14 @@ func TestCloudCredentialAttributeStore(t *testing.T) { c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ - OwnerIdentityName: "alice@external", + OwnerIdentityName: "alice@canonical.com", Name: "cred-1", CloudName: "test", } err = j.Database.GetCloudCredential(ctx, &cred) c.Assert(err, qt.IsNil) c.Check(cred, jimmtest.DBObjectEquals, dbmodel.CloudCredential{ - OwnerIdentityName: "alice@external", + OwnerIdentityName: "alice@canonical.com", Name: "cred-1", CloudName: "test", AuthType: "userpass", diff --git a/internal/jimm/clouddefaults.go b/internal/jimm/clouddefaults.go index 47c93cd51..b4c4e49ee 100644 --- a/internal/jimm/clouddefaults.go +++ b/internal/jimm/clouddefaults.go @@ -5,7 +5,7 @@ import ( "strings" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" diff --git a/internal/jimm/clouddefaults_test.go b/internal/jimm/clouddefaults_test.go index 56b131a88..aaf64d095 100644 --- a/internal/jimm/clouddefaults_test.go +++ b/internal/jimm/clouddefaults_test.go @@ -10,7 +10,7 @@ import ( qt "github.com/frankban/quicktest" "github.com/google/go-cmp/cmp/cmpopts" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "gorm.io/gorm" "github.com/canonical/jimm/internal/db" @@ -42,7 +42,7 @@ func TestSetCloudDefaults(t *testing.T) { about: "defaults do not exist yet - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -82,7 +82,7 @@ func TestSetCloudDefaults(t *testing.T) { about: "set defaults without region - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -121,7 +121,7 @@ func TestSetCloudDefaults(t *testing.T) { about: "defaults already exist - defaults updated", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -174,7 +174,7 @@ func TestSetCloudDefaults(t *testing.T) { about: "cloudregion does not exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -204,7 +204,7 @@ func TestSetCloudDefaults(t *testing.T) { about: "cannot set agent-version", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -287,7 +287,7 @@ func TestUnsetCloudDefaults(t *testing.T) { about: "all ok - keys removed from the defaults map", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -342,7 +342,7 @@ func TestUnsetCloudDefaults(t *testing.T) { about: "unset without region - keys removed from the defaults map", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -396,7 +396,7 @@ func TestUnsetCloudDefaults(t *testing.T) { about: "cloudregiondefaults not found", setup: func(c *qt.C, j *jimm.JIMM) testConfig { user := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) @@ -472,12 +472,12 @@ func TestModelDefaultsForCloud(t *testing.T) { c.Assert(err, qt.Equals, nil) user := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) user1 := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } c.Assert(j.Database.DB.Create(&user1).Error, qt.IsNil) diff --git a/internal/jimm/controller.go b/internal/jimm/controller.go index 1b637fa33..cb0551d89 100644 --- a/internal/jimm/controller.go +++ b/internal/jimm/controller.go @@ -11,7 +11,7 @@ import ( "github.com/juju/juju/api/base" "github.com/juju/juju/api/controller/controller" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/version" "github.com/juju/zaputil" "github.com/juju/zaputil/zapctx" diff --git a/internal/jimm/controller_test.go b/internal/jimm/controller_test.go index 3d94c0e7a..0fcfb42de 100644 --- a/internal/jimm/controller_test.go +++ b/internal/jimm/controller_test.go @@ -18,7 +18,7 @@ import ( "github.com/juju/juju/core/life" "github.com/juju/juju/core/status" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" semversion "github.com/juju/version" "gopkg.in/macaroon.v2" @@ -98,11 +98,11 @@ func TestAddController(t *testing.T) { // TODO(Kian) We can remove these returned users, we ignore them when importing a // controller into JIMM. ci.Users = []jujuparams.CloudUserInfo{{ - UserName: "alice@external", + UserName: "alice@canonical.com", DisplayName: "Alice", Access: "admin", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", DisplayName: "Bob", Access: "add-model", }} @@ -148,7 +148,7 @@ func TestAddController(t *testing.T) { c.Assert(err, qt.IsNil) u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } alice := openfga.NewUser(&u, client) @@ -265,11 +265,11 @@ func TestAddControllerWithVault(t *testing.T) { // TODO(Kian) We can remove these returned users, we ignore them when importing a // controller into JIMM. ci.Users = []jujuparams.CloudUserInfo{{ - UserName: "alice@external", + UserName: "alice@canonical.com", DisplayName: "Alice", Access: "admin", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", DisplayName: "Bob", Access: "add-model", }} @@ -316,7 +316,7 @@ func TestAddControllerWithVault(t *testing.T) { c.Assert(err, qt.IsNil) u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } alice := openfga.NewUser(&u, ofgaClient) alice.JimmAdmin = true @@ -379,7 +379,7 @@ const testEarliestControllerVersionEnv = `clouds: cloud-credentials: - name: test-cred cloud: test - owner: alice@external + owner: alice@canonical.com type: empty controllers: - name: test1 @@ -429,10 +429,10 @@ func TestEarliestControllerVersion(t *testing.T) { const testImportModelEnv = ` users: -- username: alice@external +- username: alice@canonical.com display-name: Alice controller-access: superuser -- username: bob@external +- username: bob@canonical.com display-name: Bob controller-access: login clouds: @@ -443,7 +443,7 @@ clouds: cloud-credentials: - name: test-credential cloud: test-cloud - owner: alice@external + owner: alice@canonical.com type: empty controllers: - name: test-controller @@ -460,18 +460,18 @@ models: cloud: test-cloud region: test-region cloud-credential: test-credential - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: write - - user: charlie@external + - user: charlie@canonical.com access: read sla: level: unsupported @@ -497,7 +497,7 @@ func TestImportModel(t *testing.T) { deltas []jujuparams.Delta }{{ about: "model imported", - user: "alice@external", + user: "alice@canonical.com", controllerName: "test-controller", newOwner: "", modelUUID: "00000002-0000-0000-0000-000000000001", @@ -510,9 +510,9 @@ func TestImportModel(t *testing.T) { info.DefaultSeries = "test-series" info.CloudTag = names.NewCloudTag("test-cloud").String() info.CloudRegion = "test-region" - info.CloudCredentialTag = names.NewCloudCredentialTag("test-cloud/alice@external/test-credential").String() + info.CloudCredentialTag = names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential").String() info.CloudCredentialValidity = &trueValue - info.OwnerTag = names.NewUserTag("alice@external").String() + info.OwnerTag = names.NewUserTag("alice@canonical.com").String() info.Life = life.Alive info.Status = jujuparams.EntityStatus{ Status: status.Status("ok"), @@ -520,10 +520,10 @@ func TestImportModel(t *testing.T) { Since: &now, } info.Users = []jujuparams.ModelUserInfo{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: jujuparams.ModelAdminAccess, }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: jujuparams.ModelReadAccess, }} info.Machines = []jujuparams.ModelMachineInfo{{ @@ -534,7 +534,7 @@ func TestImportModel(t *testing.T) { }} info.SLA = &jujuparams.ModelSLAInfo{ Level: "essential", - Owner: "alice@external", + Owner: "alice@canonical.com", } info.AgentVersion = newVersion("2.1.0") return nil @@ -543,7 +543,7 @@ func TestImportModel(t *testing.T) { Entity: &jujuparams.ModelUpdate{ ModelUUID: "00000002-0000-0000-0000-000000000001", Name: "test-model", - Owner: "alice@external", + Owner: "alice@canonical.com", Life: life.Value(constants.ALIVE.String()), ControllerUUID: "00000001-0000-0000-0000-000000000001", Status: jujuparams.StatusInfo{ @@ -603,7 +603,7 @@ func TestImportModel(t *testing.T) { Valid: true, }, Owner: dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", DisplayName: "Alice", }, Controller: dbmodel.Controller{ @@ -642,9 +642,9 @@ func TestImportModel(t *testing.T) { }, }, { about: "model from local user imported", - user: "alice@external", + user: "alice@canonical.com", controllerName: "test-controller", - newOwner: "alice@external", + newOwner: "alice@canonical.com", modelUUID: "00000002-0000-0000-0000-000000000001", jimmAdmin: true, modelInfo: func(_ context.Context, info *jujuparams.ModelInfo) error { @@ -691,7 +691,7 @@ func TestImportModel(t *testing.T) { Valid: true, }, Owner: dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", DisplayName: "Alice", }, Controller: dbmodel.Controller{ @@ -730,7 +730,7 @@ func TestImportModel(t *testing.T) { }, }, { about: "new model owner is local user", - user: "alice@external", + user: "alice@canonical.com", controllerName: "test-controller", newOwner: "bob", modelUUID: "00000002-0000-0000-0000-000000000001", @@ -775,7 +775,7 @@ func TestImportModel(t *testing.T) { }, }, { about: "model not found", - user: "alice@external", + user: "alice@canonical.com", controllerName: "test-controller", newOwner: "", modelUUID: "00000002-0000-0000-0000-000000000001", @@ -786,7 +786,7 @@ func TestImportModel(t *testing.T) { expectedError: "model not found", }, { about: "fail import from local user without newOwner flag", - user: "alice@external", + user: "alice@canonical.com", controllerName: "test-controller", newOwner: "", modelUUID: "00000002-0000-0000-0000-000000000001", @@ -799,7 +799,7 @@ func TestImportModel(t *testing.T) { info.DefaultSeries = "test-series" info.CloudTag = names.NewCloudTag("test-cloud").String() info.CloudRegion = "test-region" - info.CloudCredentialTag = names.NewCloudCredentialTag("test-cloud/alice@external/unknown-credential").String() + info.CloudCredentialTag = names.NewCloudCredentialTag("test-cloud/alice@canonical.com/unknown-credential").String() info.CloudCredentialValidity = &trueValue info.OwnerTag = names.NewUserTag("local-user").String() return nil @@ -807,7 +807,7 @@ func TestImportModel(t *testing.T) { expectedError: `cannot import model from local user, try --owner to switch the model owner`, }, { about: "cloud credentials not found", - user: "alice@external", + user: "alice@canonical.com", controllerName: "test-controller", newOwner: "", modelUUID: "00000002-0000-0000-0000-000000000001", @@ -820,15 +820,15 @@ func TestImportModel(t *testing.T) { info.DefaultSeries = "test-series" info.CloudTag = names.NewCloudTag("invalid-cloud").String() info.CloudRegion = "test-region" - info.CloudCredentialTag = names.NewCloudCredentialTag("invalid-cloud/alice@external/unknown-credential").String() + info.CloudCredentialTag = names.NewCloudCredentialTag("invalid-cloud/alice@canonical.com/unknown-credential").String() info.CloudCredentialValidity = &trueValue - info.OwnerTag = names.NewUserTag("alice@external").String() + info.OwnerTag = names.NewUserTag("alice@canonical.com").String() return nil }, - expectedError: `Failed to find cloud credential for user alice@external on cloud invalid-cloud`, + expectedError: `Failed to find cloud credential for user alice@canonical.com on cloud invalid-cloud`, }, { about: "cloud region not found", - user: "alice@external", + user: "alice@canonical.com", controllerName: "test-controller", newOwner: "", modelUUID: "00000002-0000-0000-0000-000000000001", @@ -841,15 +841,15 @@ func TestImportModel(t *testing.T) { info.DefaultSeries = "test-series" info.CloudTag = names.NewCloudTag("test-cloud").String() info.CloudRegion = "unknown-region" - info.CloudCredentialTag = names.NewCloudCredentialTag("test-cloud/alice@external/test-credential").String() + info.CloudCredentialTag = names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential").String() info.CloudCredentialValidity = &trueValue - info.OwnerTag = names.NewUserTag("alice@external").String() + info.OwnerTag = names.NewUserTag("alice@canonical.com").String() return nil }, expectedError: `cloud region not found`, }, { about: "not allowed if not superuser", - user: "bob@external", + user: "bob@canonical.com", controllerName: "test-controller", newOwner: "", modelUUID: "00000002-0000-0000-0000-000000000001", @@ -862,15 +862,15 @@ func TestImportModel(t *testing.T) { info.DefaultSeries = "test-series" info.CloudTag = names.NewCloudTag("test-cloud").String() info.CloudRegion = "test-region" - info.CloudCredentialTag = names.NewCloudCredentialTag("test-cloud/alice@external/test-credential").String() + info.CloudCredentialTag = names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential").String() info.CloudCredentialValidity = &trueValue - info.OwnerTag = names.NewUserTag("alice@external").String() + info.OwnerTag = names.NewUserTag("alice@canonical.com").String() return nil }, expectedError: `unauthorized`, }, { about: "model already exists", - user: "alice@external", + user: "alice@canonical.com", controllerName: "test-controller", newOwner: "", modelUUID: "00000002-0000-0000-0000-000000000002", @@ -883,9 +883,9 @@ func TestImportModel(t *testing.T) { info.DefaultSeries = "test-series" info.CloudTag = names.NewCloudTag("test-cloud").String() info.CloudRegion = "test-region" - info.CloudCredentialTag = names.NewCloudCredentialTag("test-cloud/alice@external/test-credential").String() + info.CloudCredentialTag = names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential").String() info.CloudCredentialValidity = &trueValue - info.OwnerTag = names.NewUserTag("alice@external").String() + info.OwnerTag = names.NewUserTag("alice@canonical.com").String() return nil }, expectedError: `model already exists`, @@ -956,9 +956,9 @@ func TestImportModel(t *testing.T) { const testControllerConfigEnv = ` users: -- username: alice@external -- username: eve@external -- username: fred@external +- username: alice@canonical.com +- username: eve@canonical.com +- username: fred@canonical.com ` func TestSetControllerConfig(t *testing.T) { @@ -973,7 +973,7 @@ func TestSetControllerConfig(t *testing.T) { expectedConfig dbmodel.ControllerConfig }{{ about: "admin allowed to set config", - user: "alice@external", + user: "alice@canonical.com", args: jujuparams.ControllerConfigSet{ Config: map[string]interface{}{ "key1": "value1", @@ -992,7 +992,7 @@ func TestSetControllerConfig(t *testing.T) { }, }, { about: "add-model user - unauthorized", - user: "eve@external", + user: "eve@canonical.com", args: jujuparams.ControllerConfigSet{ Config: map[string]interface{}{ "key1": "value1", @@ -1003,7 +1003,7 @@ func TestSetControllerConfig(t *testing.T) { expectedError: "unauthorized", }, { about: "login user - unauthorized", - user: "fred@external", + user: "fred@canonical.com", args: jujuparams.ControllerConfigSet{ Config: map[string]interface{}{ "key1": "value1", @@ -1061,7 +1061,7 @@ func TestGetControllerConfig(t *testing.T) { expectedConfig dbmodel.ControllerConfig }{{ about: "admin allowed to set config", - user: "alice@external", + user: "alice@canonical.com", jimmAdmin: true, expectedConfig: dbmodel.ControllerConfig{ Name: "jimm", @@ -1071,7 +1071,7 @@ func TestGetControllerConfig(t *testing.T) { }, }, { about: "add-model user - unauthorized", - user: "eve@external", + user: "eve@canonical.com", jimmAdmin: false, expectedConfig: dbmodel.ControllerConfig{ Name: "jimm", @@ -1081,7 +1081,7 @@ func TestGetControllerConfig(t *testing.T) { }, }, { about: "login user - unauthorized", - user: "fred@external", + user: "fred@canonical.com", jimmAdmin: false, expectedConfig: dbmodel.ControllerConfig{ Name: "jimm", @@ -1106,7 +1106,7 @@ func TestGetControllerConfig(t *testing.T) { env := jimmtest.ParseEnvironment(c, testImportModelEnv) env.PopulateDB(c, j.Database) - dbSuperuser := env.User("alice@external").DBObject(c, j.Database) + dbSuperuser := env.User("alice@canonical.com").DBObject(c, j.Database) superuser := openfga.NewUser(&dbSuperuser, nil) superuser.JimmAdmin = true @@ -1134,10 +1134,10 @@ func TestGetControllerConfig(t *testing.T) { const testUpdateMigratedModelEnv = ` users: -- username: alice@external +- username: alice@canonical.com display-name: Alice controller-access: superuser -- username: bob@external +- username: bob@canonical.com display-name: Bob controller-access: login clouds: @@ -1148,7 +1148,7 @@ clouds: cloud-credentials: - name: test-credential cloud: test-cloud - owner: alice@external + owner: alice@canonical.com type: empty controllers: - name: controller-1 @@ -1161,7 +1161,7 @@ controllers: cloud: test-cloud region: test-region-1 agent-version: 3.2.1 - admin-user: alice@external + admin-user: alice@canonical.com admin-password: c0ntr0113rs3cre7 models: - name: model-1 @@ -1172,18 +1172,18 @@ models: cloud: test-cloud region: test-region cloud-credential: test-credential - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: write - - user: charlie@external + - user: charlie@canonical.com access: read sla: level: unsupported @@ -1203,24 +1203,24 @@ func TestUpdateMigratedModel(t *testing.T) { expectedError string }{{ about: "add-model user not allowed to update migrated model", - user: "bob@external", + user: "bob@canonical.com", expectedError: "unauthorized", }, { about: "model not found", - user: "alice@external", + user: "alice@canonical.com", model: names.NewModelTag("unknown-model"), expectedError: "model not found", jimmAdmin: true, }, { about: "controller not found", - user: "alice@external", + user: "alice@canonical.com", model: names.NewModelTag("00000002-0000-0000-0000-000000000002"), targetController: "no-such-controller", expectedError: "controller not found", jimmAdmin: true, }, { about: "api returns an error", - user: "alice@external", + user: "alice@canonical.com", model: names.NewModelTag("00000002-0000-0000-0000-000000000002"), targetController: "controller-2", modelInfo: func(context.Context, *jujuparams.ModelInfo) error { @@ -1230,7 +1230,7 @@ func TestUpdateMigratedModel(t *testing.T) { jimmAdmin: true, }, { about: "all ok", - user: "alice@external", + user: "alice@canonical.com", model: names.NewModelTag("00000002-0000-0000-0000-000000000002"), targetController: "controller-2", modelInfo: func(context.Context, *jujuparams.ModelInfo) error { @@ -1285,10 +1285,10 @@ func TestUpdateMigratedModel(t *testing.T) { const testGetControllerAccessEnv = ` users: -- username: alice@external +- username: alice@canonical.com display-name: Alice controller-access: superuser -- username: bob@external +- username: bob@canonical.com display-name: Bob controller-access: login ` @@ -1313,29 +1313,29 @@ func TestGetControllerAccess(t *testing.T) { env := jimmtest.ParseEnvironment(c, testGetControllerAccessEnv) env.PopulateDBAndPermissions(c, j.ResourceTag(), j.Database, client) - dbUser := env.User("alice@external").DBObject(c, j.Database) + dbUser := env.User("alice@canonical.com").DBObject(c, j.Database) alice := openfga.NewUser(&dbUser, client) alice.JimmAdmin = true - access, err := j.GetJimmControllerAccess(ctx, alice, names.NewUserTag("alice@external")) + access, err := j.GetJimmControllerAccess(ctx, alice, names.NewUserTag("alice@canonical.com")) c.Assert(err, qt.IsNil) c.Check(access, qt.Equals, "superuser") - access, err = j.GetJimmControllerAccess(ctx, alice, names.NewUserTag("bob@external")) + access, err = j.GetJimmControllerAccess(ctx, alice, names.NewUserTag("bob@canonical.com")) c.Assert(err, qt.IsNil) c.Check(access, qt.Equals, "login") - access, err = j.GetJimmControllerAccess(ctx, alice, names.NewUserTag("charlie@external")) + access, err = j.GetJimmControllerAccess(ctx, alice, names.NewUserTag("charlie@canonical.com")) c.Assert(err, qt.IsNil) c.Check(access, qt.Equals, "login") - dbUser = env.User("bob@external").DBObject(c, j.Database) + dbUser = env.User("bob@canonical.com").DBObject(c, j.Database) alice = openfga.NewUser(&dbUser, client) - access, err = j.GetJimmControllerAccess(ctx, alice, names.NewUserTag("bob@external")) + access, err = j.GetJimmControllerAccess(ctx, alice, names.NewUserTag("bob@canonical.com")) c.Assert(err, qt.IsNil) c.Check(access, qt.Equals, "login") - _, err = j.GetJimmControllerAccess(ctx, alice, names.NewUserTag("alice@external")) + _, err = j.GetJimmControllerAccess(ctx, alice, names.NewUserTag("alice@canonical.com")) c.Assert(err, qt.ErrorMatches, "unauthorized") } @@ -1347,7 +1347,7 @@ const testInitiateMigrationEnv = `clouds: cloud-credentials: - name: test-cred cloud: test-cloud - owner: alice@external + owner: alice@canonical.com type: empty controllers: - name: controller-1 @@ -1369,18 +1369,18 @@ models: cloud: test-cloud region: test-region-1 cloud-credential: test-cred - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: write - - user: charlie@external + - user: charlie@canonical.com access: read sla: level: unsupported @@ -1393,18 +1393,18 @@ models: cloud: test-cloud region: test-region-1 cloud-credential: test-cred - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: write - - user: charlie@external + - user: charlie@canonical.com access: read sla: level: unsupported @@ -1437,7 +1437,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", }, client, ) @@ -1446,7 +1446,7 @@ func TestInitiateMigration(t *testing.T) { ModelTag: mt1.String(), TargetInfo: jujuparams.MigrationTargetInfo{ ControllerTag: names.NewControllerTag(uuid.NewString()).String(), - AuthTag: names.NewUserTag("target-user@external").String(), + AuthTag: names.NewUserTag("target-user@canonical.com").String(), Macaroons: string(macaroonData), }, }, @@ -1462,7 +1462,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", }, client, ) @@ -1471,7 +1471,7 @@ func TestInitiateMigration(t *testing.T) { ModelTag: names.NewModelTag(uuid.NewString()).String(), TargetInfo: jujuparams.MigrationTargetInfo{ ControllerTag: names.NewControllerTag(uuid.NewString()).String(), - AuthTag: names.NewUserTag("target-user@external").String(), + AuthTag: names.NewUserTag("target-user@canonical.com").String(), Macaroons: string(macaroonData), }, }, @@ -1482,7 +1482,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", }, client, ) @@ -1491,7 +1491,7 @@ func TestInitiateMigration(t *testing.T) { ModelTag: mt1.String(), TargetInfo: jujuparams.MigrationTargetInfo{ ControllerTag: names.NewControllerTag(uuid.NewString()).String(), - AuthTag: names.NewUserTag("target-user@external").String(), + AuthTag: names.NewUserTag("target-user@canonical.com").String(), }, }, initiateMigrationResults: []result{{ @@ -1503,7 +1503,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", }, client, ) @@ -1512,7 +1512,7 @@ func TestInitiateMigration(t *testing.T) { ModelTag: mt1.String(), TargetInfo: jujuparams.MigrationTargetInfo{ ControllerTag: names.NewControllerTag(uuid.NewString()).String(), - AuthTag: names.NewUserTag("target-user@external").String(), + AuthTag: names.NewUserTag("target-user@canonical.com").String(), }, }, initiateMigrationResults: []result{{}}, @@ -1522,7 +1522,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", }, client, ) @@ -1531,7 +1531,7 @@ func TestInitiateMigration(t *testing.T) { ModelTag: "invalid-model-tag", TargetInfo: jujuparams.MigrationTargetInfo{ ControllerTag: names.NewControllerTag(uuid.NewString()).String(), - AuthTag: names.NewUserTag("target-user@external").String(), + AuthTag: names.NewUserTag("target-user@canonical.com").String(), }, }, initiateMigrationResults: []result{{}}, @@ -1541,7 +1541,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", }, client, ) @@ -1550,7 +1550,7 @@ func TestInitiateMigration(t *testing.T) { ModelTag: mt1.String(), TargetInfo: jujuparams.MigrationTargetInfo{ ControllerTag: "invalid-controller-tag", - AuthTag: names.NewUserTag("target-user@external").String(), + AuthTag: names.NewUserTag("target-user@canonical.com").String(), }, }, initiateMigrationResults: []result{{}}, @@ -1560,7 +1560,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", }, client, ) @@ -1579,7 +1579,7 @@ func TestInitiateMigration(t *testing.T) { user: func(client *openfga.OFGAClient) *openfga.User { return openfga.NewUser( &dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", }, client, ) @@ -1588,7 +1588,7 @@ func TestInitiateMigration(t *testing.T) { ModelTag: mt1.String(), TargetInfo: jujuparams.MigrationTargetInfo{ ControllerTag: names.NewControllerTag(uuid.NewString()).String(), - AuthTag: names.NewUserTag("target-user@external").String(), + AuthTag: names.NewUserTag("target-user@canonical.com").String(), Macaroons: "invalid-macaroon-data", }, }, diff --git a/internal/jimm/credentials/credentials.go b/internal/jimm/credentials/credentials.go index c4a0982fb..239854042 100644 --- a/internal/jimm/credentials/credentials.go +++ b/internal/jimm/credentials/credentials.go @@ -6,7 +6,7 @@ import ( "context" "time" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/lestrrat-go/jwx/v2/jwk" ) diff --git a/internal/jimm/export_test.go b/internal/jimm/export_test.go index 02414ea4a..505ca4418 100644 --- a/internal/jimm/export_test.go +++ b/internal/jimm/export_test.go @@ -6,7 +6,7 @@ import ( "context" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" diff --git a/internal/jimm/identitymodeldefaults_test.go b/internal/jimm/identitymodeldefaults_test.go index c6495e46f..9741c1a27 100644 --- a/internal/jimm/identitymodeldefaults_test.go +++ b/internal/jimm/identitymodeldefaults_test.go @@ -38,7 +38,7 @@ func TestSetIdentityModelDefaults(t *testing.T) { about: "defaults do not exist yet - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { identity := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) @@ -63,7 +63,7 @@ func TestSetIdentityModelDefaults(t *testing.T) { about: "defaults already exist - defaults updated", setup: func(c *qt.C, j *jimm.JIMM) testConfig { identity := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) @@ -98,7 +98,7 @@ func TestSetIdentityModelDefaults(t *testing.T) { about: "cannot set agent-version", setup: func(c *qt.C, j *jimm.JIMM) testConfig { identity := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) @@ -164,7 +164,7 @@ func TestIdentityModelDefaults(t *testing.T) { about: "defaults do not exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { identity := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) @@ -177,7 +177,7 @@ func TestIdentityModelDefaults(t *testing.T) { about: "defaults exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { identity := dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", } c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index a0aa4ce59..af6ce42f3 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -16,7 +16,7 @@ import ( "github.com/juju/juju/api/base" "github.com/juju/juju/core/crossmodel" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "github.com/lestrrat-go/jwx/v2/jwt" "go.uber.org/zap" @@ -50,11 +50,6 @@ type JIMM struct { // the data. Database db.Database - // Authenticator is the authenticator JIMM uses to determine the user - // authenticating with the API. If this is not specified then all - // authentication requests are considered to have failed. - Authenticator Authenticator - // Dialer is the API dialer JIMM uses to contact juju controllers. if // this is not configured all connection attempts will fail. Dialer Dialer @@ -415,7 +410,13 @@ func (j *JIMM) AddAuditLogEntry(ale *dbmodel.AuditLogEntry) { } } -var sensitiveMethods = map[string]struct{}{"login": {}, "addcredentials": {}, "updatecredentials": {}} +var sensitiveMethods = map[string]struct{}{ + "login": {}, + "logindevice": {}, + "getdevicesessiontoken": {}, + "loginwithsessiontoken": {}, + "addcredentials": {}, + "updatecredentials": {}} var redactJSON = dbmodel.JSON(`{"params":"redacted"}`) func redactSensitiveParams(ale *dbmodel.AuditLogEntry) { diff --git a/internal/jimm/jimm_test.go b/internal/jimm/jimm_test.go index 42e092734..166cfae15 100644 --- a/internal/jimm/jimm_test.go +++ b/internal/jimm/jimm_test.go @@ -13,7 +13,7 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/db" @@ -45,15 +45,15 @@ func TestFindAuditEvents(t *testing.T) { err = j.Database.Migrate(ctx, true) c.Assert(err, qt.Equals, nil) - admin := openfga.NewUser(&dbmodel.Identity{Name: "alice@external"}, client) + admin := openfga.NewUser(&dbmodel.Identity{Name: "alice@canonical.com"}, client) err = admin.SetControllerAccess(ctx, j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) - privileged := openfga.NewUser(&dbmodel.Identity{Name: "bob@external"}, client) + privileged := openfga.NewUser(&dbmodel.Identity{Name: "bob@canonical.com"}, client) err = privileged.SetControllerAccess(ctx, j.ResourceTag(), ofganames.AuditLogViewerRelation) c.Assert(err, qt.IsNil) - unprivileged := openfga.NewUser(&dbmodel.Identity{Name: "eve@external"}, client) + unprivileged := openfga.NewUser(&dbmodel.Identity{Name: "eve@canonical.com"}, client) events := []dbmodel.AuditLogEntry{{ Time: now, @@ -172,7 +172,7 @@ const testListCoControllersEnv = `clouds: cloud-credentials: - name: test-cred cloud: test - owner: alice@external + owner: alice@canonical.com type: empty controllers: - name: test1 @@ -191,11 +191,11 @@ controllers: region: test-region-3 agent-version: 2.1.0 users: -- username: alice@external +- username: alice@canonical.com controller-access: superuser -- username: bob@external +- username: bob@canonical.com controller-access: login -- username: eve@external +- username: eve@canonical.com controller-access: "no-access" ` @@ -230,7 +230,7 @@ func TestListControllers(t *testing.T) { expectedError string }{{ about: "superuser can list controllers", - user: env.User("alice@external").DBObject(c, j.Database), + user: env.User("alice@canonical.com").DBObject(c, j.Database), jimmAdmin: true, expectedControllers: []dbmodel.Controller{ env.Controller("test1").DBObject(c, j.Database), @@ -239,11 +239,11 @@ func TestListControllers(t *testing.T) { }, }, { about: "add-model user can not list controllers", - user: env.User("bob@external").DBObject(c, j.Database), + user: env.User("bob@canonical.com").DBObject(c, j.Database), expectedError: "unauthorized", }, { about: "user withouth access rights cannot list controllers", - user: env.User("eve@external").DBObject(c, j.Database), + user: env.User("eve@canonical.com").DBObject(c, j.Database), expectedError: "unauthorized", }} @@ -270,7 +270,7 @@ const testSetControllerDeprecatedEnv = `clouds: cloud-credentials: - name: test-cred cloud: test - owner: alice@external + owner: alice@canonical.com type: empty controllers: - name: test1 @@ -279,11 +279,11 @@ controllers: region: test-region-1 agent-version: 3.2.1 users: -- username: alice@external +- username: alice@canonical.com controller-access: superuser -- username: bob@external +- username: bob@canonical.com controller-access: login -- username: eve@external +- username: eve@canonical.com controller-access: "no-access" ` @@ -318,17 +318,17 @@ func TestSetControllerDeprecated(t *testing.T) { expectedError string }{{ about: "superuser can deprecate a controller", - user: env.User("alice@external").DBObject(c, j.Database), + user: env.User("alice@canonical.com").DBObject(c, j.Database), jimmAdmin: true, deprecated: true, }, { about: "superuser can deprecate a controller", - user: env.User("alice@external").DBObject(c, j.Database), + user: env.User("alice@canonical.com").DBObject(c, j.Database), jimmAdmin: true, deprecated: false, }, { about: "user withouth access rights cannot deprecate a controller", - user: env.User("eve@external").DBObject(c, j.Database), + user: env.User("eve@canonical.com").DBObject(c, j.Database), expectedError: "unauthorized", deprecated: true, }} @@ -359,15 +359,15 @@ const removeControllerTestEnv = `clouds: regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud users: -- username: alice@external +- username: alice@canonical.com controller-access: superuser -- username: bob@external +- username: bob@canonical.com controller-access: login -- username: eve@external +- username: eve@canonical.com controller-access: "no-access" controllers: - name: controller-1 @@ -383,18 +383,18 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: write - - user: charlie@external + - user: charlie@canonical.com access: read sla: level: unsupported @@ -416,30 +416,30 @@ func TestRemoveController(t *testing.T) { expectedError string }{{ about: "superuser can remove an unavailable controller", - user: "alice@external", + user: "alice@canonical.com", unavailableSince: &now, force: true, jimmAdmin: true, }, { about: "superuser can remove a live controller with force", - user: "alice@external", + user: "alice@canonical.com", force: true, jimmAdmin: true, }, { about: "superuser cannot remove a live controller", - user: "alice@external", + user: "alice@canonical.com", force: false, jimmAdmin: true, expectedError: "controller is still alive", }, { about: "add-model user cannot remove a controller", - user: "bob@external", + user: "bob@canonical.com", expectedError: "unauthorized", jimmAdmin: false, force: false, }, { about: "user withouth access rights cannot remove a controller", - user: "eve@external", + user: "eve@canonical.com", expectedError: "unauthorized", jimmAdmin: false, force: false, @@ -498,15 +498,15 @@ const fullModelStatusTestEnv = `clouds: regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud users: -- username: alice@external +- username: alice@canonical.com controller-access: superuser -- username: bob@external +- username: bob@canonical.com controller-access: login -- username: eve@external +- username: eve@canonical.com controller-access: "no-access" controllers: - name: controller-1 @@ -522,7 +522,7 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive ` @@ -565,7 +565,7 @@ func TestFullModelStatus(t *testing.T) { expectedError string }{{ about: "superuser allowed to see full model status", - user: "alice@external", + user: "alice@canonical.com", modelUUID: "00000002-0000-0000-0000-000000000001", jimmAdmin: true, statusFunc: func(_ context.Context, _ []string) (*jujuparams.FullStatus, error) { @@ -574,7 +574,7 @@ func TestFullModelStatus(t *testing.T) { expectedStatus: fullStatus, }, { about: "model not found", - user: "alice@external", + user: "alice@canonical.com", modelUUID: "00000002-0000-0000-0000-000000000002", jimmAdmin: true, statusFunc: func(_ context.Context, _ []string) (*jujuparams.FullStatus, error) { @@ -583,7 +583,7 @@ func TestFullModelStatus(t *testing.T) { expectedError: "model not found", }, { about: "controller returns an error", - user: "alice@external", + user: "alice@canonical.com", modelUUID: "00000002-0000-0000-0000-000000000001", jimmAdmin: true, statusFunc: func(_ context.Context, _ []string) (*jujuparams.FullStatus, error) { @@ -592,7 +592,7 @@ func TestFullModelStatus(t *testing.T) { expectedError: "an error", }, { about: "add-model user not allowed to see full model status", - user: "bob@external", + user: "bob@canonical.com", modelUUID: "00000002-0000-0000-0000-000000000001", jimmAdmin: false, statusFunc: func(_ context.Context, _ []string) (*jujuparams.FullStatus, error) { @@ -601,7 +601,7 @@ func TestFullModelStatus(t *testing.T) { expectedError: "unauthorized", }, { about: "no-access user not allowed to see full model status", - user: "eve@external", + user: "eve@canonical.com", modelUUID: "00000002-0000-0000-0000-000000000001", jimmAdmin: false, statusFunc: func(_ context.Context, _ []string) (*jujuparams.FullStatus, error) { @@ -673,7 +673,7 @@ func TestFillMigrationTarget(t *testing.T) { expectedError string }{{ about: "controller exists", - userTag: "alice@external", + userTag: "alice@canonical.com", controllerName: "controller-1", expectedInfo: jujuparams.MigrationTargetInfo{ ControllerTag: "controller-00000001-0000-0000-0000-000000000001", @@ -683,7 +683,7 @@ func TestFillMigrationTarget(t *testing.T) { }, }, { about: "controller doesn't exist", - userTag: "alice@external", + userTag: "alice@canonical.com", controllerName: "controller-2", expectedError: "controller not found", }, @@ -723,7 +723,7 @@ const InitiateMigrationTestEnv = `clouds: regions: - name: test-cloud-region cloud-credentials: - - owner: alice@external + - owner: alice@canonical.com name: cred-1 cloud: test-cloud controllers: @@ -740,10 +740,10 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive users: - - username: alice@external + - username: alice@canonical.com controller-access: superuser ` @@ -760,11 +760,11 @@ func TestInitiateInternalMigration(t *testing.T) { expectedError string }{{ about: "success", - user: "alice@external", + user: "alice@canonical.com", migrateInfo: params.MigrateModelInfo{ModelTag: "model-00000002-0000-0000-0000-000000000001", TargetController: "myController"}, }, { about: "model doesn't exist", - user: "alice@external", + user: "alice@canonical.com", migrateInfo: params.MigrateModelInfo{ModelTag: "model-00000002-0000-0000-0000-000000000002", TargetController: "myController"}, expectedError: "model not found", }, diff --git a/internal/jimm/model.go b/internal/jimm/model.go index cb17d0817..b0c725872 100644 --- a/internal/jimm/model.go +++ b/internal/jimm/model.go @@ -12,7 +12,7 @@ import ( jujupermission "github.com/juju/juju/core/permission" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" @@ -716,8 +716,7 @@ func (j *JIMM) ModelInfo(ctx context.Context, user *openfga.User, mt names.Model for username, access := range userAccess { // If the user does not contain an "@" sign (no domain), it means // this is a local user of this controller and JIMM does not - // care or know about local users - only Candid users are - // relevant. + // care or know about local users. if !strings.Contains(username, "@") { continue } diff --git a/internal/jimm/model_status_parser.go b/internal/jimm/model_status_parser.go index de9eaeb5a..5cafcb026 100644 --- a/internal/jimm/model_status_parser.go +++ b/internal/jimm/model_status_parser.go @@ -14,7 +14,7 @@ import ( "github.com/juju/juju/cmd/juju/status" "github.com/juju/juju/cmd/juju/storage" rpcparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" ) diff --git a/internal/jimm/model_status_parser_test.go b/internal/jimm/model_status_parser_test.go index 99ec5e460..9933cf05a 100644 --- a/internal/jimm/model_status_parser_test.go +++ b/internal/jimm/model_status_parser_test.go @@ -27,11 +27,11 @@ clouds: regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud users: -- username: alice@external +- username: alice@canonical.com controller-access: superuser controllers: - name: controller-1 @@ -47,14 +47,14 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin - name: model-2 type: iaas @@ -64,14 +64,14 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin - name: model-3 type: iaas @@ -81,14 +81,14 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin - name: model-5 type: iaas @@ -98,14 +98,14 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin sla: level: unsupported diff --git a/internal/jimm/model_test.go b/internal/jimm/model_test.go index baf2eb3b7..e4e92e37e 100644 --- a/internal/jimm/model_test.go +++ b/internal/jimm/model_test.go @@ -16,7 +16,7 @@ import ( "github.com/google/uuid" "github.com/juju/juju/core/life" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/version/v2" "sigs.k8s.io/yaml" @@ -42,20 +42,20 @@ func TestModelCreateArgs(t *testing.T) { about: "all ok", args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), CloudTag: names.NewCloudTag("test-cloud").String(), - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1").String(), }, expectedArgs: jimm.ModelCreateArgs{ Name: "test-model", - Owner: names.NewUserTag("alice@external"), + Owner: names.NewUserTag("alice@canonical.com"), Cloud: names.NewCloudTag("test-cloud"), - CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1"), + CloudCredential: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1"), }, }, { about: "name not specified", args: jujuparams.ModelCreateArgs{ - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), CloudTag: names.NewCloudTag("test-cloud").String(), CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice/test-credential-1").String(), }, @@ -64,16 +64,16 @@ func TestModelCreateArgs(t *testing.T) { about: "invalid owner tag", args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: "alice@external", + OwnerTag: "alice@canonical.com", CloudTag: names.NewCloudTag("test-cloud").String(), CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice/test-credential-1").String(), }, - expectedError: `"alice@external" is not a valid tag`, + expectedError: `"alice@canonical.com" is not a valid tag`, }, { about: "invalid cloud tag", args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), CloudTag: "test-cloud", CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice/test-credential-1").String(), }, @@ -82,7 +82,7 @@ func TestModelCreateArgs(t *testing.T) { about: "invalid cloud credential tag", args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), CloudTag: names.NewCloudTag("test-cloud").String(), CloudCredentialTag: "test-credential-1", }, @@ -91,7 +91,7 @@ func TestModelCreateArgs(t *testing.T) { about: "cloud does not match cloud credential cloud", args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), CloudTag: names.NewCloudTag("test-cloud").String(), CloudCredentialTag: names.NewCloudCredentialTag("another-cloud/alice/test-credential-1").String(), }, @@ -108,7 +108,7 @@ func TestModelCreateArgs(t *testing.T) { about: "cloud tag not specified", args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice/test-credential-1").String(), }, expectedError: "no cloud specified for model; please specify one", @@ -159,26 +159,26 @@ clouds: regions: - name: test-region-1 users: - - user: alice@external + - user: alice@canonical.com access: add-model user-defaults: -- user: alice@external +- user: alice@canonical.com defaults: key4: value4 cloud-defaults: -- user: alice@external +- user: alice@canonical.com cloud: test-cloud region: test-region-1 defaults: key1: value1 key2: value2 -- user: alice@external +- user: alice@canonical.com cloud: test-cloud defaults: key3: value3 cloud-credentials: - name: test-credential-1 - owner: alice@external + owner: alice@canonical.com cloud: test-cloud auth-type: empty controllers: @@ -217,19 +217,19 @@ status: info: running a test life: alive users: -- user: alice@external +- user: alice@canonical.com access: admin - user: bob access: read `[1:])), - username: "alice@external", + username: "alice@canonical.com", jimmAdmin: true, args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-region-1", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1").String(), }, expectModel: dbmodel.Model{ Name: "test-model", @@ -238,7 +238,7 @@ users: Valid: true, }, Owner: dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", }, Controller: dbmodel.Controller{ Name: "controller-2", @@ -272,26 +272,26 @@ clouds: regions: - name: test-region-1 users: - - user: alice@external + - user: alice@canonical.com access: add-model user-defaults: -- user: alice@external +- user: alice@canonical.com defaults: key4: value4 cloud-defaults: -- user: alice@external +- user: alice@canonical.com cloud: test-cloud region: test-region-1 defaults: key1: value1 key2: value2 -- user: alice@external +- user: alice@canonical.com cloud: test-cloud defaults: key3: value3 cloud-credentials: - name: test-credential-1 - owner: alice@external + owner: alice@canonical.com cloud: test-cloud auth-type: empty controllers: @@ -330,20 +330,20 @@ status: info: running a test life: alive users: -- user: alice@external +- user: alice@canonical.com access: admin - user: bob access: read `[1:])), - username: "alice@external", + username: "alice@canonical.com", jimmAdmin: true, args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), CloudTag: names.NewCloudTag("test-cloud").String(), // Creating a model without specifying the cloud region CloudRegion: "", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1").String(), }, expectModel: dbmodel.Model{ Name: "test-model", @@ -352,7 +352,7 @@ users: Valid: true, }, Owner: dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", }, Controller: dbmodel.Controller{ Name: "controller-2", @@ -386,11 +386,11 @@ clouds: regions: - name: test-region-1 users: - - user: alice@external + - user: alice@canonical.com access: add-model cloud-credentials: - name: test-credential-1 - owner: alice@external + owner: alice@canonical.com cloud: test-cloud auth-type: empty controllers: @@ -424,19 +424,19 @@ status: info: running a test life: alive users: -- user: alice@external +- user: alice@canonical.com access: admin - user: bob access: read `[1:]), - username: "alice@external", + username: "alice@canonical.com", jimmAdmin: true, args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-region-1", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1").String(), }, expectModel: dbmodel.Model{ Name: "test-model", @@ -445,7 +445,7 @@ users: Valid: true, }, Owner: dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", }, Controller: dbmodel.Controller{ Name: "controller-2", @@ -479,13 +479,13 @@ clouds: regions: - name: test-region-1 users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: add-model cloud-credentials: - name: test-credential-1 - owner: alice@external + owner: alice@canonical.com cloud: test-cloud auth-type: empty controllers: @@ -506,9 +506,9 @@ controllers: region: test-region-1 priority: 2 users: -- username: alice@external +- username: alice@canonical.com controller-access: superuser -- username: bob@external +- username: bob@canonical.com controller-access: login `[1:], updateCredential: func(_ context.Context, _ jujuparams.TaggedCredential) ([]jujuparams.UpdateCredentialModelResult, error) { @@ -524,19 +524,19 @@ status: info: running a test life: alive users: -- user: alice@external +- user: alice@canonical.com access: admin - user: bob access: read `[1:]), - username: "alice@external", + username: "alice@canonical.com", jimmAdmin: true, args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("bob@external").String(), + OwnerTag: names.NewUserTag("bob@canonical.com").String(), CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-region-1", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1").String(), }, expectModel: dbmodel.Model{ Name: "test-model", @@ -545,7 +545,7 @@ users: Valid: true, }, Owner: dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", }, Controller: dbmodel.Controller{ Name: "controller-2", @@ -579,11 +579,11 @@ clouds: regions: - name: test-region-1 users: - - user: alice@external + - user: alice@canonical.com access: add-model cloud-credentials: - name: test-credential-1 - owner: alice@external + owner: alice@canonical.com cloud: test-cloud auth-type: empty controllers: @@ -604,9 +604,9 @@ controllers: region: test-region-1 priority: 2 users: -- username: alice@external +- username: alice@canonical.com controller-access: login -- username: bob@external +- username: bob@canonical.com controller-access: login `[1:], updateCredential: func(_ context.Context, _ jujuparams.TaggedCredential) ([]jujuparams.UpdateCredentialModelResult, error) { @@ -622,18 +622,18 @@ status: info: running a test life: alive users: -- user: alice@external +- user: alice@canonical.com access: admin - user: bob access: read `[1:]), - username: "alice@external", + username: "alice@canonical.com", args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("bob@external").String(), + OwnerTag: names.NewUserTag("bob@canonical.com").String(), CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-region-1", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1").String(), }, expectError: "unauthorized", }, { @@ -645,11 +645,11 @@ clouds: regions: - name: test-region-1 users: - - user: alice@external + - user: alice@canonical.com access: add-model cloud-credentials: - name: test-credential-1 - owner: alice@external + owner: alice@canonical.com cloud: test-cloud auth-type: empty controllers: @@ -679,14 +679,14 @@ controllers: createModel: func(ctx context.Context, args *jujuparams.ModelCreateArgs, mi *jujuparams.ModelInfo) error { return errors.E("a test error") }, - username: "alice@external", + username: "alice@canonical.com", jimmAdmin: true, args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-region-1", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1").String(), }, expectError: "a test error", }, { @@ -698,7 +698,7 @@ clouds: regions: - name: test-region-1 users: - - user: alice@external + - user: alice@canonical.com access: add-model - name: test-cloud-2 type: test-provider @@ -706,11 +706,11 @@ clouds: - name: test-region-2 cloud-credentials: - name: test-credential-1 - owner: alice@external + owner: alice@canonical.com cloud: test-cloud auth-type: empty - name: test-credential-2 - owner: alice@external + owner: alice@canonical.com cloud: test-cloud-2 auth-type: empty controllers: @@ -732,7 +732,7 @@ controllers: priority: 1 models: - name: test-model - owner: alice@external + owner: alice@canonical.com uuid: 00000001-0000-0000-0000-0000-000000000003 cloud: test-cloud region: test-region-1 @@ -752,21 +752,21 @@ status: info: running a test life: alive users: -- user: alice@external +- user: alice@canonical.com access: admin - user: bob access: read `[1:]), - username: "alice@external", + username: "alice@canonical.com", jimmAdmin: true, args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-region-1", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1").String(), }, - expectError: "model alice@external/test-model already exists", + expectError: "model alice@canonical.com/test-model already exists", }, { name: "UpdateCredentialError", env: ` @@ -776,11 +776,11 @@ clouds: regions: - name: test-region-1 users: - - user: alice@external + - user: alice@canonical.com access: add-model cloud-credentials: - name: test-credential-1 - owner: alice@external + owner: alice@canonical.com cloud: test-cloud auth-type: empty controllers: @@ -814,19 +814,19 @@ status: info: running a test life: alive users: -- user: alice@external +- user: alice@canonical.com access: admin - user: bob access: read `[1:]), - username: "alice@external", + username: "alice@canonical.com", jimmAdmin: true, args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-region-1", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1").String(), }, expectError: "failed to update cloud credential: a silly error", }, { @@ -839,7 +839,7 @@ clouds: - name: test-region-1 cloud-credentials: - name: test-credential-1 - owner: alice@external + owner: alice@canonical.com cloud: test-cloud auth-type: empty controllers: @@ -873,17 +873,17 @@ status: info: running a test life: alive users: -- user: alice@external +- user: alice@canonical.com access: admin `[1:]), - username: "alice@external", + username: "alice@canonical.com", jimmAdmin: true, args: jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-region-1", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1").String(), }, expectError: "unauthorized", }} @@ -994,14 +994,14 @@ func assertConfig(config map[string]interface{}, fnc func(context.Context, *juju } -// Note that this env does not give the everyone@external user access to the model. +// Note that this env does not give the everyone user access to the model. const modelInfoTestEnv = `clouds: - name: test-cloud type: test-provider regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud controllers: @@ -1018,7 +1018,7 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive status: status: available @@ -1028,15 +1028,15 @@ models: level: unsupported agent-version: 1.2.3 users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: write - - user: charlie@external + - user: charlie@canonical.com access: read ` -// This env extends the one above to provide the everyone@external user with access to the model. +// This env extends the one above to provide the everyone user with access to the model. const modelInfoTestEnvWithEveryoneAccess = modelInfoTestEnv + ` - user: everyone@external access: read @@ -1052,7 +1052,7 @@ var modelInfoTests = []struct { }{{ name: "AdminUser", env: modelInfoTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectModelInfo: &jujuparams.ModelInfo{ Name: "model-1", @@ -1063,8 +1063,8 @@ var modelInfoTests = []struct { DefaultSeries: "warty", CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-cloud-region", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/cred-1").String(), - OwnerTag: names.NewUserTag("alice@external").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/cred-1").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), Life: life.Value(constants.ALIVE.String()), Status: jujuparams.EntityStatus{ Status: "available", @@ -1072,13 +1072,13 @@ var modelInfoTests = []struct { Since: newDate(2020, 2, 20, 20, 2, 20, 0, time.UTC), }, Users: []jujuparams.ModelUserInfo{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "write", }, { - UserName: "charlie@external", + UserName: "charlie@canonical.com", Access: "read", }}, Machines: []jujuparams.ModelMachineInfo{{ @@ -1106,7 +1106,7 @@ var modelInfoTests = []struct { }, { name: "WriteUser", env: modelInfoTestEnv, - username: "bob@external", + username: "bob@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectModelInfo: &jujuparams.ModelInfo{ Name: "model-1", @@ -1117,8 +1117,8 @@ var modelInfoTests = []struct { DefaultSeries: "warty", CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-cloud-region", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/cred-1").String(), - OwnerTag: names.NewUserTag("alice@external").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/cred-1").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), Life: life.Value(constants.ALIVE.String()), Status: jujuparams.EntityStatus{ Status: "available", @@ -1126,7 +1126,7 @@ var modelInfoTests = []struct { Since: newDate(2020, 2, 20, 20, 2, 20, 0, time.UTC), }, Users: []jujuparams.ModelUserInfo{{ - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "write", }}, Machines: []jujuparams.ModelMachineInfo{{ @@ -1154,7 +1154,7 @@ var modelInfoTests = []struct { }, { name: "ReadUser", env: modelInfoTestEnv, - username: "charlie@external", + username: "charlie@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectModelInfo: &jujuparams.ModelInfo{ Name: "model-1", @@ -1165,8 +1165,8 @@ var modelInfoTests = []struct { DefaultSeries: "warty", CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-cloud-region", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/cred-1").String(), - OwnerTag: names.NewUserTag("alice@external").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/cred-1").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), Life: life.Value(constants.ALIVE.String()), Status: jujuparams.EntityStatus{ Status: "available", @@ -1174,7 +1174,7 @@ var modelInfoTests = []struct { Since: newDate(2020, 2, 20, 20, 2, 20, 0, time.UTC), }, Users: []jujuparams.ModelUserInfo{{ - UserName: "charlie@external", + UserName: "charlie@canonical.com", Access: "read", }}, SLA: &jujuparams.ModelSLAInfo{ @@ -1185,19 +1185,19 @@ var modelInfoTests = []struct { }, { name: "NoAccess", env: modelInfoTestEnv, - username: "diane@external", + username: "diane@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: "unauthorized", }, { name: "NotFound", env: modelInfoTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000002", expectError: "model not found", }, { name: "Access through everyone user", env: modelInfoTestEnvWithEveryoneAccess, - username: "diane@external", + username: "diane@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectModelInfo: &jujuparams.ModelInfo{ Name: "model-1", @@ -1208,8 +1208,8 @@ var modelInfoTests = []struct { DefaultSeries: "warty", CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-cloud-region", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/cred-1").String(), - OwnerTag: names.NewUserTag("alice@external").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/cred-1").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), Life: life.Value(constants.ALIVE.String()), Status: jujuparams.EntityStatus{ Status: "available", @@ -1254,8 +1254,8 @@ func TestModelInfo(t *testing.T) { mi.DefaultSeries = "warty" mi.CloudTag = names.NewCloudTag("test-cloud").String() mi.CloudRegion = "test-cloud-region" - mi.CloudCredentialTag = names.NewCloudCredentialTag("test-cloud/alice@external/cred-1").String() - mi.OwnerTag = names.NewUserTag("alice@external").String() + mi.CloudCredentialTag = names.NewCloudCredentialTag("test-cloud/alice@canonical.com/cred-1").String() + mi.OwnerTag = names.NewUserTag("alice@canonical.com").String() mi.Life = life.Value(constants.ALIVE.String()) mi.Status = jujuparams.EntityStatus{ Status: "available", @@ -1321,7 +1321,7 @@ const modelStatusTestEnv = `clouds: regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud controllers: @@ -1338,16 +1338,16 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: write - - user: charlie@external + - user: charlie@canonical.com access: read users: -- username: diane@external +- username: diane@canonical.com controller-access: superuser ` @@ -1361,13 +1361,13 @@ var modelStatusTests = []struct { expectError string }{{ name: "ModelNotFound", - username: "alice@external", + username: "alice@canonical.com", uuid: "00000001-0000-0000-0000-000000000001", expectError: `model not found`, }, { name: "UnauthorizedUser", env: modelStatusTestEnv, - username: "bob@external", + username: "bob@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: "unauthorized", }, { @@ -1382,10 +1382,10 @@ var modelStatusTests = []struct { ms.HostedMachineCount = 10 ms.ApplicationCount = 3 ms.UnitCount = 20 - ms.OwnerTag = names.NewUserTag("alice@external").String() + ms.OwnerTag = names.NewUserTag("alice@canonical.com").String() return nil }, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectModelStatus: &jujuparams.ModelStatus{ ModelTag: names.NewModelTag("00000002-0000-0000-0000-000000000001").String(), @@ -1394,7 +1394,7 @@ var modelStatusTests = []struct { HostedMachineCount: 10, ApplicationCount: 3, UnitCount: 20, - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), }, }, { name: "APIError", @@ -1402,7 +1402,7 @@ var modelStatusTests = []struct { modelStatus: func(_ context.Context, ms *jujuparams.ModelStatus) error { return errors.E("test error") }, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: "test error", }} @@ -1459,7 +1459,7 @@ const forEachModelTestEnv = `clouds: regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud controllers: @@ -1476,16 +1476,16 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: admin sla: level: unsupported @@ -1501,16 +1501,16 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: write sla: level: unsupported @@ -1525,14 +1525,14 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin sla: level: unsupported @@ -1547,22 +1547,22 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: read sla: level: unsupported agent-version: 1.2.3 users: -- username: alice@external +- username: alice@canonical.com controller-access: superuser ` @@ -1590,7 +1590,7 @@ func TestForEachUserModel(t *testing.T) { env := jimmtest.ParseEnvironment(c, forEachModelTestEnv) env.PopulateDBAndPermissions(c, j.ResourceTag(), j.Database, client) - dbUser := env.User("bob@external").DBObject(c, j.Database) + dbUser := env.User("bob@canonical.com").DBObject(c, j.Database) user := openfga.NewUser(&dbUser, client) var res []jujuparams.ModelSummaryResult @@ -1611,8 +1611,8 @@ func TestForEachUserModel(t *testing.T) { DefaultSeries: "warty", CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-cloud-region", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/cred-1").String(), - OwnerTag: names.NewUserTag("alice@external").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/cred-1").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), Life: life.Value(constants.ALIVE.String()), Status: jujuparams.EntityStatus{ Status: "available", @@ -1645,8 +1645,8 @@ func TestForEachUserModel(t *testing.T) { DefaultSeries: "warty", CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-cloud-region", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/cred-1").String(), - OwnerTag: names.NewUserTag("alice@external").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/cred-1").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), Life: life.Value(constants.ALIVE.String()), Status: jujuparams.EntityStatus{ Status: "available", @@ -1679,8 +1679,8 @@ func TestForEachUserModel(t *testing.T) { DefaultSeries: "warty", CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-cloud-region", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/cred-1").String(), - OwnerTag: names.NewUserTag("alice@external").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/cred-1").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), Life: life.Value(constants.ALIVE.String()), Status: jujuparams.EntityStatus{ Status: "available", @@ -1729,7 +1729,7 @@ func TestForEachModel(t *testing.T) { env := jimmtest.ParseEnvironment(c, forEachModelTestEnv) env.PopulateDBAndPermissions(c, j.ResourceTag(), j.Database, client) - dbUser := env.User("bob@external").DBObject(c, j.Database) + dbUser := env.User("bob@canonical.com").DBObject(c, j.Database) bob := openfga.NewUser(&dbUser, client) err = j.ForEachModel(ctx, bob, func(_ *dbmodel.Model, _ jujuparams.UserAccessPermission) error { @@ -1738,7 +1738,7 @@ func TestForEachModel(t *testing.T) { c.Check(err, qt.ErrorMatches, `unauthorized`) c.Assert(errors.ErrorCode(err), qt.Equals, errors.CodeUnauthorized) - dbUser = env.User("alice@external").DBObject(c, j.Database) + dbUser = env.User("alice@canonical.com").DBObject(c, j.Database) alice := openfga.NewUser(&dbUser, client) alice.JimmAdmin = true @@ -1763,7 +1763,7 @@ const grantModelAccessTestEnv = `clouds: regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud controllers: @@ -1778,11 +1778,11 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: charlie@external + - user: charlie@canonical.com access: write ` @@ -1799,158 +1799,158 @@ var grantModelAccessTests = []struct { expectErrorCode errors.Code }{{ name: "ModelNotFound", - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "write", expectError: `model not found`, expectErrorCode: errors.CodeNotFound, }, { name: "Admin grants 'admin' access to a user with no access", env: grantModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "admin", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin grants 'write' access to a user with no access", env: grantModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "write", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin grants 'read' access to a user with no access", env: grantModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "read", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin grants 'write' access to a user who already has 'write' access", env: grantModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "charlie@external", + targetUsername: "charlie@canonical.com", access: "write", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin grants 'read' access to a user who already has 'write' access", env: grantModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "charlie@external", + targetUsername: "charlie@canonical.com", access: "read", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin grants 'admin' access to themselves", env: grantModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "alice@external", + targetUsername: "alice@canonical.com", access: "admin", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin grants 'write' access to themselves", env: grantModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "alice@external", + targetUsername: "alice@canonical.com", access: "write", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin grants 'read' access to themselves", env: grantModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "alice@external", + targetUsername: "alice@canonical.com", access: "read", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "UserNotAuthorized", env: grantModelAccessTestEnv, - username: "charlie@external", + username: "charlie@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "write", expectError: `unauthorized`, expectErrorCode: errors.CodeUnauthorized, @@ -1958,17 +1958,17 @@ var grantModelAccessTests = []struct { name: "DialError", env: grantModelAccessTestEnv, dialError: errors.E("test dial error"), - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "write", expectError: `test dial error`, }, { name: "unknown access", env: grantModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "some-unknown-access", expectError: `failed to recognize given access: "some-unknown-access"`, }} @@ -2028,7 +2028,7 @@ const revokeModelAccessTestEnv = `clouds: regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud controllers: @@ -2043,15 +2043,15 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: admin - - user: charlie@external + - user: charlie@canonical.com access: write - - user: daphne@external + - user: daphne@canonical.com access: read - name: model-2 uuid: 00000002-0000-0000-0000-000000000002 @@ -2059,11 +2059,11 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: earl@external + - user: earl@canonical.com access: admin ` @@ -2082,594 +2082,594 @@ var revokeModelAccessTests = []struct { expectErrorCode errors.Code }{{ name: "ModelNotFound", - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "write", expectError: `model not found`, expectErrorCode: errors.CodeNotFound, }, { name: "Admin revokes 'admin' access from another admin", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "admin", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin revokes 'write' access from another admin", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "write", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin revokes 'read' access from another admin", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "read", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin revokes 'admin' access from a user who has 'write' access", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "charlie@external", + targetUsername: "charlie@canonical.com", access: "admin", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin revokes 'write' access from a user who has 'write' access", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "charlie@external", + targetUsername: "charlie@canonical.com", access: "write", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin revokes 'read' access from a user who has 'write' access", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "charlie@external", + targetUsername: "charlie@canonical.com", access: "read", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin revokes 'admin' access from a user who has 'read' access", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "daphne@external", + targetUsername: "daphne@canonical.com", access: "admin", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin revokes 'write' access from a user who has 'read' access", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "daphne@external", + targetUsername: "daphne@canonical.com", access: "write", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin revokes 'read' access from a user who has 'read' access", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "daphne@external", + targetUsername: "daphne@canonical.com", access: "read", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin revokes 'admin' access from themselves", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "alice@external", + targetUsername: "alice@canonical.com", access: "admin", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin revokes 'write' access from themselves", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "alice@external", + targetUsername: "alice@canonical.com", access: "write", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin revokes 'read' access from themselves", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "alice@external", + targetUsername: "alice@canonical.com", access: "read", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Writer revokes 'admin' access from themselves", env: revokeModelAccessTestEnv, - username: "charlie@external", + username: "charlie@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "charlie@external", + targetUsername: "charlie@canonical.com", access: "admin", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Writer revokes 'write' access from themselves", env: revokeModelAccessTestEnv, - username: "charlie@external", + username: "charlie@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "charlie@external", + targetUsername: "charlie@canonical.com", access: "write", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Writer revokes 'read' access from themselves", env: revokeModelAccessTestEnv, - username: "charlie@external", + username: "charlie@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "charlie@external", + targetUsername: "charlie@canonical.com", access: "read", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Reader revokes 'admin' access from themselves", env: revokeModelAccessTestEnv, - username: "daphne@external", + username: "daphne@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "daphne@external", + targetUsername: "daphne@canonical.com", access: "admin", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Reader revokes 'write' access from themselves", env: revokeModelAccessTestEnv, - username: "daphne@external", + username: "daphne@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "daphne@external", + targetUsername: "daphne@canonical.com", access: "write", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Reader revokes 'read' access from themselves", env: revokeModelAccessTestEnv, - username: "daphne@external", + username: "daphne@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "daphne@external", + targetUsername: "daphne@canonical.com", access: "read", expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin revokes 'admin' access from a user who has separate tuples for all accesses (read/write/admin)", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "daphne@external", + targetUsername: "daphne@canonical.com", access: "admin", extraInitialTuples: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, // No need to add the 'read' relation, because it's already there due to the environment setup. }, expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin revokes 'write' access from a user who has separate tuples for all accesses (read/write/admin)", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "daphne@external", + targetUsername: "daphne@canonical.com", access: "write", extraInitialTuples: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, // No need to add the 'read' relation, because it's already there due to the environment setup. }, expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "Admin revokes 'read' access from a user who has separate tuples for all accesses (read/write/admin)", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "daphne@external", + targetUsername: "daphne@canonical.com", access: "read", extraInitialTuples: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, // No need to add the 'read' relation, because it's already there due to the environment setup. }, expectRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("charlie@external")), + Object: ofganames.ConvertTag(names.NewUserTag("charlie@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, expectRemovedRelations: []openfga.Tuple{{ - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.WriterRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }, { - Object: ofganames.ConvertTag(names.NewUserTag("daphne@external")), + Object: ofganames.ConvertTag(names.NewUserTag("daphne@canonical.com")), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag("00000002-0000-0000-0000-000000000001")), }}, }, { name: "UserNotAuthorized", env: revokeModelAccessTestEnv, - username: "charlie@external", + username: "charlie@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "write", expectError: `unauthorized`, expectErrorCode: errors.CodeUnauthorized, @@ -2677,17 +2677,17 @@ var revokeModelAccessTests = []struct { name: "DialError", env: revokeModelAccessTestEnv, dialError: errors.E("test dial error"), - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "write", expectError: `test dial error`, }, { name: "unknown access", env: revokeModelAccessTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", - targetUsername: "bob@external", + targetUsername: "bob@canonical.com", access: "some-unknown-access", expectError: `failed to recognize given access: "some-unknown-access"`, }} @@ -2769,7 +2769,7 @@ const destroyModelTestEnv = `clouds: regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud controllers: @@ -2784,15 +2784,15 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: write users: -- username: charlie@external +- username: charlie@canonical.com controller-access: superuser ` @@ -2812,14 +2812,14 @@ var destroyModelTests = []struct { }{{ name: "NotFound", env: destroyModelTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000002", expectError: `model not found`, expectErrorCode: errors.CodeNotFound, }, { name: "Unauthorized", env: destroyModelTestEnv, - username: "bob@external", + username: "bob@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: `unauthorized`, expectErrorCode: errors.CodeUnauthorized, @@ -2844,7 +2844,7 @@ var destroyModelTests = []struct { } return nil }, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", destroyStorage: newBool(true), force: newBool(false), @@ -2856,13 +2856,13 @@ var destroyModelTests = []struct { destroyModel: func(_ context.Context, _ names.ModelTag, _, _ *bool, _, _ *time.Duration) error { return nil }, - username: "charlie@external", + username: "charlie@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", }, { name: "DialError", env: destroyModelTestEnv, dialError: errors.E("dial error"), - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: `dial error`, }, { @@ -2871,7 +2871,7 @@ var destroyModelTests = []struct { destroyModel: func(_ context.Context, _ names.ModelTag, _, _ *bool, _, _ *time.Duration) error { return errors.E("api error") }, - username: "charlie@external", + username: "charlie@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: `api error`, }} @@ -2948,14 +2948,14 @@ var dumpModelTests = []struct { name: "NotFound", // reuse the destroyModelTestEnv for these tests. env: destroyModelTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000002", expectError: `model not found`, expectErrorCode: errors.CodeNotFound, }, { name: "Unauthorized", env: destroyModelTestEnv, - username: "bob@external", + username: "bob@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: `unauthorized`, expectErrorCode: errors.CodeUnauthorized, @@ -2971,7 +2971,7 @@ var dumpModelTests = []struct { } return "model dump", nil }, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", simplified: true, expectString: "model dump", @@ -2981,14 +2981,14 @@ var dumpModelTests = []struct { dumpModel: func(_ context.Context, _ names.ModelTag, _ bool) (string, error) { return "model dump2", nil }, - username: "charlie@external", + username: "charlie@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectString: "model dump2", }, { name: "DialError", env: destroyModelTestEnv, dialError: errors.E("dial error"), - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: `dial error`, }, { @@ -2997,7 +2997,7 @@ var dumpModelTests = []struct { dumpModel: func(_ context.Context, _ names.ModelTag, _ bool) (string, error) { return "", errors.E("api error") }, - username: "charlie@external", + username: "charlie@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: `api error`, }} @@ -3065,14 +3065,14 @@ var dumpModelDBTests = []struct { name: "NotFound", // reuse the destroyModelTestEnv for these tests. env: destroyModelTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000002", expectError: `model not found`, expectErrorCode: errors.CodeNotFound, }, { name: "Unauthorized", env: destroyModelTestEnv, - username: "bob@external", + username: "bob@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: `unauthorized`, expectErrorCode: errors.CodeUnauthorized, @@ -3085,7 +3085,7 @@ var dumpModelDBTests = []struct { } return map[string]interface{}{"model": "dump"}, nil }, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectDump: map[string]interface{}{"model": "dump"}, }, { @@ -3094,14 +3094,14 @@ var dumpModelDBTests = []struct { dumpModelDB: func(_ context.Context, _ names.ModelTag) (map[string]interface{}, error) { return map[string]interface{}{"model": "dump 2"}, nil }, - username: "charlie@external", + username: "charlie@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectDump: map[string]interface{}{"model": "dump 2"}, }, { name: "DialError", env: destroyModelTestEnv, dialError: errors.E("dial error"), - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: `dial error`, }, { @@ -3110,7 +3110,7 @@ var dumpModelDBTests = []struct { dumpModelDB: func(_ context.Context, _ names.ModelTag) (map[string]interface{}, error) { return nil, errors.E("api error") }, - username: "charlie@external", + username: "charlie@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: `api error`, }} @@ -3178,14 +3178,14 @@ var validateModelUpgradeTests = []struct { name: "NotFound", // reuse the destroyModelTestEnv for these tests. env: destroyModelTestEnv, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000002", expectError: `model not found`, expectErrorCode: errors.CodeNotFound, }, { name: "Unauthorized", env: destroyModelTestEnv, - username: "bob@external", + username: "bob@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: `unauthorized`, expectErrorCode: errors.CodeUnauthorized, @@ -3201,7 +3201,7 @@ var validateModelUpgradeTests = []struct { } return nil }, - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", force: true, }, { @@ -3213,13 +3213,13 @@ var validateModelUpgradeTests = []struct { } return nil }, - username: "charlie@external", + username: "charlie@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", }, { name: "DialError", env: destroyModelTestEnv, dialError: errors.E("dial error"), - username: "alice@external", + username: "alice@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: `dial error`, }, { @@ -3228,7 +3228,7 @@ var validateModelUpgradeTests = []struct { validateModelUpgrade: func(_ context.Context, _ names.ModelTag, _ bool) error { return errors.E("api error") }, - username: "charlie@external", + username: "charlie@canonical.com", uuid: "00000002-0000-0000-0000-000000000001", expectError: `api error`, }} @@ -3288,10 +3288,10 @@ const updateModelCredentialTestEnv = `clouds: regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-2 cloud: test-cloud -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud controllers: @@ -3306,11 +3306,11 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: charlie@external + - user: charlie@canonical.com access: write ` @@ -3330,7 +3330,7 @@ var updateModelCredentialTests = []struct { name: "success", env: updateModelCredentialTestEnv, updateCredential: func(_ context.Context, taggedCredential jujuparams.TaggedCredential) ([]jujuparams.UpdateCredentialModelResult, error) { - if taggedCredential.Tag != "cloudcred-test-cloud_alice@external_cred-2" { + if taggedCredential.Tag != "cloudcred-test-cloud_alice@canonical.com_cred-2" { return nil, errors.E("bad cloud credential tag") } return nil, nil @@ -3339,13 +3339,13 @@ var updateModelCredentialTests = []struct { if modelTag.Id() != "00000002-0000-0000-0000-000000000001" { return errors.E("bad model tag") } - if credentialTag.Id() != "test-cloud/alice@external/cred-2" { + if credentialTag.Id() != "test-cloud/alice@canonical.com/cred-2" { return errors.E("bad cloud credential tag") } return nil }, - username: "alice@external", - credential: "test-cloud/alice@external/cred-2", + username: "alice@canonical.com", + credential: "test-cloud/alice@canonical.com/cred-2", uuid: "00000002-0000-0000-0000-000000000001", expectModel: dbmodel.Model{ Name: "model-1", @@ -3354,7 +3354,7 @@ var updateModelCredentialTests = []struct { Valid: true, }, Owner: dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", }, Controller: dbmodel.Controller{ Name: "controller-1", @@ -3377,7 +3377,7 @@ var updateModelCredentialTests = []struct { name: "user not admin", env: updateModelCredentialTestEnv, updateCredential: func(_ context.Context, taggedCredential jujuparams.TaggedCredential) ([]jujuparams.UpdateCredentialModelResult, error) { - if taggedCredential.Tag != "cloudcred-test-cloud_alice@external_cred-2" { + if taggedCredential.Tag != "cloudcred-test-cloud_alice@canonical.com_cred-2" { return nil, errors.E("bad cloud credential tag") } return nil, nil @@ -3386,21 +3386,21 @@ var updateModelCredentialTests = []struct { if modelTag.Id() != "00000002-0000-0000-0000-000000000001" { return errors.E("bad model tag") } - if credentialTag.Id() != "test-cloud/alice@external/cred-2" { + if credentialTag.Id() != "test-cloud/alice@canonical.com/cred-2" { return errors.E("bad cloud credential tag") } return nil }, - username: "charlie@external", - credential: "test-cloud/alice@external/cred-2", + username: "charlie@canonical.com", + credential: "test-cloud/alice@canonical.com/cred-2", uuid: "00000002-0000-0000-0000-000000000001", expectError: "unauthorized", expectErrorCode: errors.CodeUnauthorized, }, { name: "model not found", env: updateModelCredentialTestEnv, - username: "charlie@external", - credential: "test-cloud/alice@external/cred-2", + username: "charlie@canonical.com", + credential: "test-cloud/alice@canonical.com/cred-2", uuid: "00000002-0000-0000-0000-000000000002", expectError: "unauthorized", expectErrorCode: errors.CodeUnauthorized, @@ -3408,7 +3408,7 @@ var updateModelCredentialTests = []struct { name: "credential not found", env: updateModelCredentialTestEnv, updateCredential: func(_ context.Context, taggedCredential jujuparams.TaggedCredential) ([]jujuparams.UpdateCredentialModelResult, error) { - if taggedCredential.Tag != "cloudcred-test-cloud_alice@external_cred-2" { + if taggedCredential.Tag != "cloudcred-test-cloud_alice@canonical.com_cred-2" { return nil, errors.E("bad cloud credential tag") } return nil, nil @@ -3417,15 +3417,15 @@ var updateModelCredentialTests = []struct { if modelTag.Id() != "00000002-0000-0000-0000-000000000001" { return errors.E("bad model tag") } - if credentialTag.Id() != "test-cloud/alice@external/cred-2" { + if credentialTag.Id() != "test-cloud/alice@canonical.com/cred-2" { return errors.E("bad cloud credential tag") } return nil }, - username: "alice@external", - credential: "test-cloud/alice@external/cred-3", + username: "alice@canonical.com", + credential: "test-cloud/alice@canonical.com/cred-3", uuid: "00000002-0000-0000-0000-000000000001", - expectError: `cloudcredential "test-cloud/alice@external/cred-3" not found`, + expectError: `cloudcredential "test-cloud/alice@canonical.com/cred-3" not found`, expectErrorCode: errors.CodeNotFound, }, { name: "update credential returns an error", @@ -3433,15 +3433,15 @@ var updateModelCredentialTests = []struct { updateCredential: func(_ context.Context, taggedCredential jujuparams.TaggedCredential) ([]jujuparams.UpdateCredentialModelResult, error) { return nil, errors.E("an error") }, - username: "alice@external", - credential: "test-cloud/alice@external/cred-2", + username: "alice@canonical.com", + credential: "test-cloud/alice@canonical.com/cred-2", uuid: "00000002-0000-0000-0000-000000000001", expectError: "an error", }, { name: "change model credential returns an error", env: updateModelCredentialTestEnv, updateCredential: func(_ context.Context, taggedCredential jujuparams.TaggedCredential) ([]jujuparams.UpdateCredentialModelResult, error) { - if taggedCredential.Tag != "cloudcred-test-cloud_alice@external_cred-2" { + if taggedCredential.Tag != "cloudcred-test-cloud_alice@canonical.com_cred-2" { return nil, errors.E("bad cloud credential tag") } return nil, nil @@ -3449,8 +3449,8 @@ var updateModelCredentialTests = []struct { changeModelCredential: func(_ context.Context, modelTag names.ModelTag, credentialTag names.CloudCredentialTag) error { return errors.E("an error") }, - username: "alice@external", - credential: "test-cloud/alice@external/cred-2", + username: "alice@canonical.com", + credential: "test-cloud/alice@canonical.com/cred-2", uuid: "00000002-0000-0000-0000-000000000001", expectError: "an error", }} @@ -3534,7 +3534,7 @@ status: info: running a test life: alive users: -- user: alice@external +- user: alice@canonical.com access: admin - user: bob access: read @@ -3566,11 +3566,11 @@ clouds: - name: test-region-1 - name: test-region-2 users: -- username: alice@external +- username: alice@canonical.com controller-access: superuser cloud-credentials: - name: test-credential-1 - owner: alice@external + owner: alice@canonical.com cloud: test-cloud auth-type: empty controllers: @@ -3602,7 +3602,7 @@ controllers: env := jimmtest.ParseEnvironment(c, envDefinition) env.PopulateDBAndPermissions(c, j.ResourceTag(), j.Database, client) - dbUser := env.User("alice@external").DBObject(c, j.Database) + dbUser := env.User("alice@canonical.com").DBObject(c, j.Database) user := openfga.NewUser(&dbUser, client) controller := dbmodel.Controller{ @@ -3617,10 +3617,10 @@ controllers: args := jimm.ModelCreateArgs{} err = args.FromJujuModelCreateArgs(&jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("alice@external").String(), + OwnerTag: names.NewUserTag("alice@canonical.com").String(), CloudTag: names.NewCloudTag("test-cloud").String(), CloudRegion: "test-region-1", - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@external/test-credential-1").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1").String(), }) c.Assert(err, qt.IsNil) diff --git a/internal/jimm/modelsummary.go b/internal/jimm/modelsummary.go index 0582531d5..cf8ae49df 100644 --- a/internal/jimm/modelsummary.go +++ b/internal/jimm/modelsummary.go @@ -7,7 +7,7 @@ import ( "sort" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil" "github.com/juju/zaputil/zapctx" diff --git a/internal/jimm/modelsummary_test.go b/internal/jimm/modelsummary_test.go index 9563f4f70..a1ff9a2b6 100644 --- a/internal/jimm/modelsummary_test.go +++ b/internal/jimm/modelsummary_test.go @@ -46,7 +46,7 @@ func TestWatchAllModelSummaries(t *testing.T) { UUID: modelUUID, Controller: controllerName, Name: "test-model", - Admins: []string{"alice@external", "bob@external"}, + Admins: []string{"alice@canonical.com", "bob@canonical.com"}, Cloud: "test-cloud", Region: "test-cloud-region", Size: jujuparams.ModelSummarySize{ @@ -124,7 +124,7 @@ func TestWatchAllModelSummaries(t *testing.T) { Name: "test-model", Cloud: "test-cloud", Region: "test-cloud-region", - Admins: []string{"alice@external", "bob@external"}, + Admins: []string{"alice@canonical.com", "bob@canonical.com"}, Size: jujuparams.ModelSummarySize{ Machines: 0, Containers: 0, diff --git a/internal/jimm/service_account_test.go b/internal/jimm/service_account_test.go index 00ffa8b6d..2da83030f 100644 --- a/internal/jimm/service_account_test.go +++ b/internal/jimm/service_account_test.go @@ -7,7 +7,7 @@ import ( "testing" qt "github.com/frankban/quicktest" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" @@ -30,7 +30,7 @@ func TestAddServiceAccount(t *testing.T) { c.Assert(err, qt.IsNil) user := openfga.NewUser( &dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", DisplayName: "Bob", }, client, @@ -42,7 +42,7 @@ func TestAddServiceAccount(t *testing.T) { c.Assert(err, qt.IsNil) userAlice := openfga.NewUser( &dbmodel.Identity{ - Name: "alive@external", + Name: "alive@canonical.com", DisplayName: "Alice", }, client, diff --git a/internal/jimm/user.go b/internal/jimm/user.go index a3afcaedc..f9975569c 100644 --- a/internal/jimm/user.go +++ b/internal/jimm/user.go @@ -5,61 +5,12 @@ package jimm import ( "context" - jujuparams "github.com/juju/juju/rpc/params" - "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/openfga" ) -// Authenticate processes the given LoginRequest using the configured -// authenticator, it then retrieves the user information from the database. -// If the authenticated user does not yet exist in the database it will be -// created using the values returned from the authenticator as the user's -// details. Finally we check if the user is a administrator of JIMM and set -// the JimmAdmin field if this is true which will persist for the duration -// of the websocket connection. -func (j *JIMM) Authenticate(ctx context.Context, req *jujuparams.LoginRequest) (*openfga.User, error) { - const op = errors.Op("jimm.Authenticate") - if j == nil || j.Authenticator == nil { - return nil, errors.E(op, errors.CodeServerConfiguration, "authenticator not configured") - } - - u, err := j.Authenticator.Authenticate(ctx, req) - if err != nil { - return nil, errors.E(op, err) - } - - err = j.Database.Transaction(func(tx *db.Database) error { - pu := dbmodel.Identity{ - Name: u.Name, - } - if err := tx.GetIdentity(ctx, &pu); err != nil { - return err - } - u.Model = pu.Model - u.LastLogin = pu.LastLogin - - // TODO(mhilton) support disabled users. - if u.DisplayName != "" { - pu.DisplayName = u.DisplayName - } - pu.LastLogin.Time = j.Database.DB.Config.NowFunc() - pu.LastLogin.Valid = true - return tx.UpdateIdentity(ctx, &pu) - }) - if err != nil { - return nil, errors.E(op, err) - } - isJimmAdmin, err := openfga.IsAdministrator(ctx, u, j.ResourceTag()) - if err != nil { - return nil, errors.E(op, err) - } - u.JimmAdmin = isJimmAdmin - return u, nil -} - // GetOpenFGAUserAndAuthorise returns a valid OpenFGA user, authorising // them as an admin of JIMM if a tuple exists for this user. func (j *JIMM) GetOpenFGAUserAndAuthorise(ctx context.Context, email string) (*openfga.User, error) { diff --git a/internal/jimm/user_test.go b/internal/jimm/user_test.go index bd4909776..1e55c516d 100644 --- a/internal/jimm/user_test.go +++ b/internal/jimm/user_test.go @@ -4,101 +4,19 @@ package jimm_test import ( "context" - "database/sql" "testing" "time" qt "github.com/frankban/quicktest" - jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/db" - "github.com/canonical/jimm/internal/dbmodel" - "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/jimmtest" - "github.com/canonical/jimm/internal/openfga" ofganames "github.com/canonical/jimm/internal/openfga/names" ) -func TestAuthenticateNoAuthenticator(t *testing.T) { - c := qt.New(t) - - j := &jimm.JIMM{} - _, err := j.Authenticate(context.Background(), &jujuparams.LoginRequest{}) - c.Check(err, qt.ErrorMatches, `authenticator not configured`) - c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeServerConfiguration) -} - -func TestAuthenticate(t *testing.T) { - c := qt.New(t) - - client, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) - c.Assert(err, qt.IsNil) - - var auth jimmtest.Authenticator - now := time.Now().UTC().Round(time.Millisecond) - j := &jimm.JIMM{ - UUID: "test", - Database: db.Database{ - DB: jimmtest.PostgresDB(c, func() time.Time { return now }), - }, - Authenticator: &auth, - OpenFGAClient: client, - } - ctx := context.Background() - - err = j.Database.Migrate(ctx, false) - c.Assert(err, qt.IsNil) - - auth.User = openfga.NewUser( - &dbmodel.Identity{ - Name: "bob@external", - DisplayName: "Bob", - }, - client, - ) - u, err := j.Authenticate(ctx, nil) - c.Assert(err, qt.IsNil) - c.Check(u.Name, qt.Equals, "bob@external") - c.Check(u.JimmAdmin, qt.IsFalse) - - err = auth.User.SetControllerAccess( - context.Background(), - names.NewControllerTag(j.UUID), - ofganames.AdministratorRelation, - ) - c.Assert(err, qt.IsNil) - - u, err = j.Authenticate(ctx, nil) - c.Assert(err, qt.IsNil) - c.Check(u.Name, qt.Equals, "bob@external") - c.Check(u.JimmAdmin, qt.IsTrue) - - u2 := dbmodel.Identity{ - Name: "bob@external", - } - err = j.Database.GetIdentity(ctx, &u2) - c.Assert(err, qt.IsNil) - - c.Check(u2, qt.DeepEquals, dbmodel.Identity{ - Model: u.Model, - Name: "bob@external", - DisplayName: "Bob", - LastLogin: sql.NullTime{ - Time: now, - Valid: true, - }, - }) - - auth.Err = errors.E("test error", errors.CodeUnauthorized) - u, err = j.Authenticate(ctx, nil) - c.Check(err, qt.ErrorMatches, `test error`) - c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeUnauthorized) - c.Check(u, qt.IsNil) -} - func TestGetOpenFGAUser(t *testing.T) { c := qt.New(t) @@ -132,10 +50,10 @@ func TestGetOpenFGAUser(t *testing.T) { c.Assert(err, qt.IsNil) // Get the OpenFGA variant of the user - ofgaUser, err := j.GetOpenFGAUserAndAuthorise(ctx, "bob@external.com") + ofgaUser, err := j.GetOpenFGAUserAndAuthorise(ctx, "bob@canonical.com.com") c.Assert(err, qt.IsNil) // Username -> email - c.Assert(ofgaUser.Name, qt.Equals, "bob@external.com") + c.Assert(ofgaUser.Name, qt.Equals, "bob@canonical.com.com") // As no display name was set for this user as they're being created this time over c.Assert(ofgaUser.DisplayName, qt.Equals, "") // The last login should be updated, so we check if it's been updated @@ -156,10 +74,10 @@ func TestGetOpenFGAUser(t *testing.T) { qt.IsNil, ) - ofgaUser, err = j.GetOpenFGAUserAndAuthorise(ctx, "bob@external.com") + ofgaUser, err = j.GetOpenFGAUserAndAuthorise(ctx, "bob@canonical.com.com") c.Assert(err, qt.IsNil) - c.Assert(ofgaUser.Name, qt.Equals, "bob@external.com") + c.Assert(ofgaUser.Name, qt.Equals, "bob@canonical.com.com") c.Assert(ofgaUser.DisplayName, qt.Equals, "") c.Assert((time.Since(ofgaUser.LastLogin.Time) > time.Second), qt.IsFalse) c.Assert(ofgaUser.LastLogin.Valid, qt.IsTrue) diff --git a/internal/jimm/watcher.go b/internal/jimm/watcher.go index 44efde1a5..2ec0e4bcc 100644 --- a/internal/jimm/watcher.go +++ b/internal/jimm/watcher.go @@ -9,7 +9,7 @@ import ( "github.com/juju/juju/core/migration" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" diff --git a/internal/jimm/watcher_test.go b/internal/jimm/watcher_test.go index f917dbcd4..05056d80c 100644 --- a/internal/jimm/watcher_test.go +++ b/internal/jimm/watcher_test.go @@ -15,7 +15,7 @@ import ( "github.com/juju/juju/core/life" "github.com/juju/juju/core/migration" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/constants" "github.com/canonical/jimm/internal/db" @@ -31,7 +31,7 @@ const testWatcherEnv = `clouds: regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud controllers: @@ -48,18 +48,18 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive status: status: available info: "OK!" since: 2020-02-20T20:02:20Z users: - - user: alice@external + - user: alice@canonical.com access: admin - - user: bob@external + - user: bob@canonical.com access: write - - user: charlie@external + - user: charlie@canonical.com access: read sla: level: unsupported @@ -72,7 +72,7 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: dying - name: model-3 type: iaas @@ -82,7 +82,7 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: dead ` @@ -331,7 +331,7 @@ var watcherTests = []struct { Entity: &jujuparams.ModelUpdate{ ModelUUID: "00000002-0000-0000-0000-000000000004", Name: "new-model", - Owner: "charlie@external", + Owner: "charlie@canonical.com", Life: "starting", }, }}, @@ -357,7 +357,7 @@ var watcherTests = []struct { Entity: &jujuparams.ModelUpdate{ ModelUUID: "00000002-0000-0000-0000-000000000001", Name: "model-1", - Owner: "alice@external", + Owner: "alice@canonical.com", Life: life.Value(constants.ALIVE.String()), ControllerUUID: "00000001-0000-0000-0000-000000000001", Status: jujuparams.StatusInfo{ @@ -638,7 +638,7 @@ var modelSummaryWatcherTests = []struct { Units: 4, Relations: 12, }, - Admins: []string{"alice@external", "bob"}, + Admins: []string{"alice@canonical.com", "bob"}, }, { // this is a summary for an model unknown to jimm // meaning its summary will not be published @@ -652,7 +652,7 @@ var modelSummaryWatcherTests = []struct { Units: 2, Relations: 1, }, - Admins: []string{"bob@external"}, + Admins: []string{"bob@canonical.com"}, }}, nil, }, @@ -668,7 +668,7 @@ var modelSummaryWatcherTests = []struct { Units: 4, Relations: 12, }, - Admins: []string{"alice@external"}, + Admins: []string{"alice@canonical.com"}, }, }) }, @@ -876,7 +876,7 @@ const testMigratingModelsWatcherEnv = `clouds: regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud controllers: @@ -898,7 +898,7 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: migrating-internal - name: model-2 type: iaas @@ -908,7 +908,7 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: migrating-away ` @@ -986,7 +986,7 @@ const testFailedMigratingModelsWatcherEnv = `clouds: regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud controllers: @@ -1008,7 +1008,7 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: migrating-internal ` @@ -1079,7 +1079,7 @@ const testWatcherIgnoreDeltasForModelsFromIncorrectControllerEnv = `clouds: regions: - name: test-cloud-region cloud-credentials: -- owner: alice@external +- owner: alice@canonical.com name: cred-1 cloud: test-cloud controllers: @@ -1100,7 +1100,7 @@ models: cloud: test-cloud region: test-cloud-region cloud-credential: cred-1 - owner: alice@external + owner: alice@canonical.com life: alive ` @@ -1177,7 +1177,7 @@ func TestWatcherIgnoreDeltasForModelsFromIncorrectController(t *testing.T) { Entity: &jujuparams.ModelUpdate{ ModelUUID: "00000002-0000-0000-0000-000000000001", Name: "model-1", - Owner: "alice@external", + Owner: "alice@canonical.com", Life: life.Value(constants.ALIVE.String()), Status: jujuparams.StatusInfo{ Current: "busy", diff --git a/internal/jimmjwx/jwt_test.go b/internal/jimmjwx/jwt_test.go index a0459f818..aa6ed8868 100644 --- a/internal/jimmjwx/jwt_test.go +++ b/internal/jimmjwx/jwt_test.go @@ -69,7 +69,7 @@ func TestNewJWTIsParsableByExponent(t *testing.T) { // Mint a new JWT tok, err := jwtService.NewJWT(ctx, jimmjwx.JWTParams{ Controller: "controller-my-diglett-controller", - User: "diglett@external", + User: "diglett@canonical.com", Access: map[string]string{ "controller": "superuser", "model": "administrator", @@ -90,7 +90,7 @@ func TestNewJWTIsParsableByExponent(t *testing.T) { // Test token has what we want c.Assert(token.Audience()[0], qt.Equals, "controller-my-diglett-controller") - c.Assert(token.Subject(), qt.Equals, "diglett@external") + c.Assert(token.Subject(), qt.Equals, "diglett@canonical.com") accessClaim, ok := token.Get("access") c.Assert(ok, qt.IsTrue) c.Assert(accessClaim, qt.DeepEquals, map[string]any{ @@ -130,7 +130,7 @@ func TestNewJWTExpires(t *testing.T) { // Mint a new JWT tok, err := jwtService.NewJWT(ctx, jimmjwx.JWTParams{ Controller: "controller-my-diglett-controller", - User: "diglett@external", + User: "diglett@canonical.com", Access: map[string]string{ "controller": "superuser", "model": "administrator", diff --git a/internal/jimmtest/api.go b/internal/jimmtest/api.go index 7858a79f6..97e17aa5b 100644 --- a/internal/jimmtest/api.go +++ b/internal/jimmtest/api.go @@ -13,7 +13,7 @@ import ( "github.com/juju/juju/core/crossmodel" jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/juju/version" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index 3c99613f1..62dc52aa3 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -4,8 +4,13 @@ package jimmtest import ( "context" + "encoding/base64" + "strings" + "time" + "github.com/juju/juju/api" jujuparams "github.com/juju/juju/rpc/params" + "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/canonical/jimm/internal/auth" @@ -13,6 +18,10 @@ import ( "github.com/canonical/jimm/internal/openfga" ) +var ( + jwtTestSecret = "test-secret" +) + // An Authenticator is an implementation of jimm.Authenticator that returns // the stored user and error. type Authenticator struct { @@ -39,3 +48,30 @@ func NewMockOAuthAuthenticator(secretKey string) MockOAuthAuthenticator { func (m MockOAuthAuthenticator) VerifySessionToken(token string, secretKey string) (jwt.Token, error) { return auth.VerifySessionToken(token, m.secretKey) } + +func NewUserSessionLogin(username string) api.LoginProvider { + email := convertUsernameToEmail(username) + token, err := jwt.NewBuilder(). + Subject(email). + Expiration(time.Now().Add(1 * time.Hour)). + Build() + if err != nil { + panic("failed to generate test session token") + } + + freshToken, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte(jwtTestSecret))) + if err != nil { + panic("failed to sign test session token") + } + + b64Token := base64.StdEncoding.EncodeToString(freshToken) + lp := api.NewSessionTokenLoginProvider(b64Token, nil, nil) + return lp +} + +func convertUsernameToEmail(username string) string { + if !strings.Contains(username, "@") { + return username + "@canonical.com" + } + return username +} diff --git a/internal/jimmtest/env.go b/internal/jimmtest/env.go index 4d5e2133c..335a2d474 100644 --- a/internal/jimmtest/env.go +++ b/internal/jimmtest/env.go @@ -10,7 +10,7 @@ import ( qt "github.com/frankban/quicktest" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "sigs.k8s.io/yaml" "github.com/canonical/jimm/internal/db" diff --git a/internal/jimmtest/jimm_mock.go b/internal/jimmtest/jimm_mock.go index 4b448a023..b22deb2e6 100644 --- a/internal/jimmtest/jimm_mock.go +++ b/internal/jimmtest/jimm_mock.go @@ -9,7 +9,7 @@ import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/google/uuid" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/version" "github.com/canonical/jimm/api/params" diff --git a/internal/jimmtest/store.go b/internal/jimmtest/store.go index cb680830c..9e542e6b6 100644 --- a/internal/jimmtest/store.go +++ b/internal/jimmtest/store.go @@ -5,7 +5,7 @@ import ( "sync" "time" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/lestrrat-go/jwx/v2/jwk" "github.com/canonical/jimm/internal/errors" diff --git a/internal/jimmtest/suite.go b/internal/jimmtest/suite.go index 1003f3d4c..01fc0d9a8 100644 --- a/internal/jimmtest/suite.go +++ b/internal/jimmtest/suite.go @@ -6,24 +6,21 @@ import ( "context" "net/http/httptest" "net/url" + "strings" "time" - "github.com/canonical/candid/candidtest" cofga "github.com/canonical/ofga" "github.com/go-chi/chi/v5" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/juju/api" "github.com/juju/juju/core/network" corejujutesting "github.com/juju/juju/juju/testing" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" - "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/discharger" "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/jimmhttp" "github.com/canonical/jimm/internal/jimmjwx" @@ -87,12 +84,12 @@ func (s *JIMMSuite) SetUpTest(c *gc.C) { s.cancel = cancel // Note that the secret key here must match what is used in tests. - s.JIMM.OAuthAuthenticator = NewMockOAuthAuthenticator("test-key") + s.JIMM.OAuthAuthenticator = NewMockOAuthAuthenticator(jwtTestSecret) err = s.JIMM.Database.Migrate(ctx, false) c.Assert(err, gc.Equals, nil) s.AdminUser = &dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", LastLogin: db.Now(), } err = s.JIMM.Database.GetIdentity(ctx, s.AdminUser) @@ -115,6 +112,9 @@ func (s *JIMMSuite) SetUpTest(c *gc.C) { "/.well-known", wellknownapi.NewWellKnownHandler(s.JIMM.CredentialStore), ) + macaroonDischarger := s.setupMacaroonDischarger(c) + localDischargePath := "/macaroons" + mux.Handle(localDischargePath+"/*", discharger.GetDischargerMux(macaroonDischarger, localDischargePath)) s.Server = httptest.NewServer(mux) @@ -146,6 +146,33 @@ func (s *JIMMSuite) TearDownTest(c *gc.C) { } } +func (s *JIMMSuite) setupMacaroonDischarger(c *gc.C) *discharger.MacaroonDischarger { + cfg := discharger.MacaroonDischargerConfig{ + MacaroonExpiryDuration: 1 * time.Hour, + ControllerUUID: s.JIMM.UUID, + } + macaroonDischarger, err := discharger.NewMacaroonDischarger(cfg, &s.JIMM.Database, s.JIMM.OpenFGAClient) + c.Assert(err, gc.IsNil) + return macaroonDischarger +} + +func (s *JIMMSuite) AddAdminUser(c *gc.C, email string) { + identity := dbmodel.Identity{ + Name: email, + } + err := s.JIMM.Database.GetIdentity(context.Background(), &identity) + c.Assert(err, gc.IsNil) + // Set the display name of the identity. + displayName, _, _ := strings.Cut(email, "@") + identity.DisplayName = displayName + err = s.JIMM.Database.UpdateIdentity(context.Background(), &identity) + c.Assert(err, gc.IsNil) + // Give the identity admin permission. + ofgaUser := openfga.NewUser(&identity, s.OFGAClient) + err = ofgaUser.SetControllerAccess(context.Background(), s.JIMM.ResourceTag(), ofganames.AdministratorRelation) + c.Assert(err, gc.IsNil) +} + func (s *JIMMSuite) NewUser(u *dbmodel.Identity) *openfga.User { return openfga.NewUser(u, s.OFGAClient) } @@ -213,50 +240,6 @@ func (s *JIMMSuite) AddModel(c *gc.C, owner names.UserTag, name string, cloud na return names.NewModelTag(mi.UUID) } -// A CandidSuite is a suite that initialises a candid test system to use a -// jimm Authenticator. -type CandidSuite struct { - // ControllerAdmins is the list of users and groups that are - // controller adminstrators. - ControllerAdmins []string - - // The following are created in SetUpTest - Candid *candidtest.Server - CandidPublicKey string - Authenticator jimm.Authenticator -} - -func (s *CandidSuite) SetUpTest(c *gc.C) { - s.Candid = candidtest.NewServer() - s.Candid.AddUser("agent-user", candidtest.GroupListGroup) - ofgaClient, _, _, err := SetupTestOFGAClient(c.TestName()) - c.Assert(err, gc.IsNil) - s.Authenticator = auth.JujuAuthenticator{ - Client: ofgaClient, - Bakery: identchecker.NewBakery(identchecker.BakeryParams{ - Locator: s.Candid, - Key: bakery.MustGenerateKey(), - IdentityClient: s.Candid.CandidClient("agent-user"), - Location: "jimmtest", - }), - ControllerAdmins: s.ControllerAdmins, - } - tpi, err := httpbakery.ThirdPartyInfoForLocation(context.Background(), nil, s.Candid.URL.String()) - c.Assert(err, gc.Equals, nil) - pk, err := tpi.PublicKey.MarshalText() - c.Assert(err, gc.Equals, nil) - s.CandidPublicKey = string(pk) - -} - -func (s *CandidSuite) TearDownTest(c *gc.C) { - s.Authenticator = nil - if s.Candid != nil { - s.Candid.Close() - s.Candid = nil - } -} - // A JujuSuite is a suite that intialises a JIMM and adds the testing juju // controller. type JujuSuite struct { @@ -302,7 +285,7 @@ type BootstrapSuite struct { func (s *BootstrapSuite) SetUpTest(c *gc.C) { s.JujuSuite.SetUpTest(c) - cct := names.NewCloudCredentialTag(TestCloudName + "/bob@external/cred") + cct := names.NewCloudCredentialTag(TestCloudName + "/bob@canonical.com/cred") s.UpdateCloudCredential(c, cct, jujuparams.CloudCredential{ AuthType: "empty", }) @@ -312,7 +295,7 @@ func (s *BootstrapSuite) SetUpTest(c *gc.C) { err := s.JIMM.Database.GetCloudCredential(ctx, s.CloudCredential) c.Assert(err, gc.Equals, nil) - mt := s.AddModel(c, names.NewUserTag("bob@external"), "model-1", names.NewCloudTag(TestCloudName), TestCloudRegionName, cct) + mt := s.AddModel(c, names.NewUserTag("bob@canonical.com"), "model-1", names.NewCloudTag(TestCloudName), TestCloudRegionName, cct) s.Model = new(dbmodel.Model) s.Model.SetTag(mt) err = s.JIMM.Database.GetModel(ctx, s.Model) diff --git a/internal/jujuapi/access_control.go b/internal/jujuapi/access_control.go index 2a5c938c4..4ac451e2a 100644 --- a/internal/jujuapi/access_control.go +++ b/internal/jujuapi/access_control.go @@ -38,7 +38,7 @@ var ( // [9] - Application offer name // [10] - Relation specifier (i.e., #member) // A complete matcher example would look like so with square-brackets denoting groups and paranthsis denoting index: - // (1)[controller](2)[-](3)[controller-1](4)[:](5)[alice@external-place](6)[/](7)[model-1](8)[.](9)[offer-1](10)[#relation-specifier]" + // (1)[controller](2)[-](3)[controller-1](4)[:](5)[alice@canonical.com-place](6)[/](7)[model-1](8)[.](9)[offer-1](10)[#relation-specifier]" // In the case of something like: user-alice@wonderland or group-alices-wonderland#member, it would look like so: // (1)[user](2)[-](3)[alices@wonderland] // (1)[group](2)[-](3)[alices-wonderland](10)[#member] diff --git a/internal/jujuapi/access_control_test.go b/internal/jujuapi/access_control_test.go index 99e90d3c6..551d1c82c 100644 --- a/internal/jujuapi/access_control_test.go +++ b/internal/jujuapi/access_control_test.go @@ -314,9 +314,9 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { }, //Test username with dots and @ -> group { - input: tuple{"user-" + "kelvin.lina.test@external", "member", "group-" + group.Name}, + input: tuple{"user-" + "kelvin.lina.test@canonical.com", "member", "group-" + group.Name}, want: createTuple( - "user:"+"kelvin.lina.test@external", + "user:"+"kelvin.lina.test@canonical.com", "member", "group:"+stringGroupID(group.ID), ), @@ -880,8 +880,8 @@ func (s *accessControlSuite) TestCheckRelationAsNonAdmin(c *gc.C) { defer conn.Close() client := api.NewClient(conn) - userAliceKey := "user-alice@external" - userBobKey := "user-bob@external" + userAliceKey := "user-alice@canonical.com" + userBobKey := "user-bob@canonical.com" // Verify Bob checking for Alice's permission fails input := apiparams.RelationshipTuple{ @@ -1348,7 +1348,7 @@ func createTestControllerEnvironment(ctx context.Context, c *gc.C, s *accessCont c.Assert(err, gc.IsNil) u := dbmodel.Identity{ - Name: petname.Generate(2, "-") + "@external", + Name: petname.Generate(2, "-") + "@canonical.com", } c.Assert(db.DB.Create(&u).Error, gc.IsNil) diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index 3a2933f44..80868248b 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -9,13 +9,12 @@ import ( "github.com/juju/juju/rpc" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/openfga" - "github.com/canonical/jimm/internal/servermon" ) // unsupportedLogin returns an appropriate error for login attempts using @@ -32,52 +31,7 @@ var facadeInit = make(map[string]func(r *controllerRoot) []int) // Login implements the Login method on the Admin facade. func (r *controllerRoot) Login(ctx context.Context, req jujuparams.LoginRequest) (jujuparams.LoginResult, error) { const op = errors.Op("jujuapi.Login") - - u, err := r.jimm.Authenticate(ctx, &req) - if err != nil { - var aerr *auth.AuthenticationError - if stderrors.As(err, &aerr) { - return aerr.LoginResult, nil - } - return jujuparams.LoginResult{}, errors.E(op, err) - } - - r.mu.Lock() - r.user = u - r.mu.Unlock() - - var facades []jujuparams.FacadeVersions - for name, f := range facadeInit { - facades = append(facades, jujuparams.FacadeVersions{ - Name: name, - Versions: f(r), - }) - } - sort.Slice(facades, func(i, j int) bool { - return facades[i].Name < facades[j].Name - }) - - servermon.LoginSuccessCount.Inc() - srvVersion, err := r.jimm.EarliestControllerVersion(ctx) - if err != nil { - return jujuparams.LoginResult{}, errors.E(op, err) - } - aui := jujuparams.AuthUserInfo{ - DisplayName: u.DisplayName, - Identity: u.Tag().String(), - // TODO(Kian) CSS-6040 improve combining Postgres and OpenFGA info - ControllerAccess: u.GetControllerAccess(ctx, r.jimm.ResourceTag()).String(), - } - if u.LastLogin.Valid { - aui.LastConnection = &u.LastLogin.Time - } - return jujuparams.LoginResult{ - PublicDNSName: r.params.PublicDNSName, - UserInfo: &aui, - ControllerTag: names.NewControllerTag(r.params.ControllerUUID).String(), - Facades: facades, - ServerVersion: srvVersion.String(), - }, nil + return jujuparams.LoginResult{}, errors.E(op, "Invalid login, ensure you are using Juju 3.5+") } // LoginDevice starts a device login flow (typically a CLI). It will return a verification URI @@ -136,7 +90,7 @@ func (r *controllerRoot) GetDeviceSessionToken(ctx context.Context) (params.GetD // TODO(ale8k): Add vault logic to get secret key and generate one // on start up. - encToken, err := authSvc.MintSessionToken(email, "secret-key") + encToken, err := authSvc.MintSessionToken(email, "test-secret") if err != nil { return response, errors.E(op, err) } @@ -156,7 +110,9 @@ func (r *controllerRoot) LoginWithSessionToken(ctx context.Context, req params.L authenticationSvc := r.jimm.OAuthAuthenticationService() // Verify the session token - jwtToken, err := authenticationSvc.VerifySessionToken(req.SessionToken, "secret-key") + // TODO(CSS-7081): Ensure for tests that the secret key can be configured. + // Or configure cmd tests to use the configured secret. + jwtToken, err := authenticationSvc.VerifySessionToken(req.SessionToken, "test-secret") if err != nil { var aerr *auth.AuthenticationError if stderrors.As(err, &aerr) { diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index 89810c6b0..4bce1d761 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -24,7 +24,6 @@ import ( jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v4" gc "gopkg.in/check.v1" - "gopkg.in/macaroon.v2" ) type adminSuite struct { @@ -57,21 +56,12 @@ func (s *adminSuite) TestLoginToController(c *gc.C) { }, "test") defer conn.Close() err := conn.Login(nil, "", "", nil) - c.Assert(err, gc.Equals, nil) + c.Assert(err, gc.ErrorMatches, "Invalid login, ensure you are using Juju 3\\.5\\+") var resp jujuparams.RedirectInfoResult err = conn.APICall("Admin", 3, "", "RedirectInfo", nil, &resp) c.Assert(jujuparams.ErrCode(err), gc.Equals, jujuparams.CodeNotImplemented) } -func (s *adminSuite) TestLoginToControllerWithInvalidMacaroon(c *gc.C) { - invalidMacaroon, err := macaroon.New(nil, []byte("invalid"), "", macaroon.V1) - c.Assert(err, gc.Equals, nil) - conn := s.open(c, &api.Info{ - Macaroons: []macaroon.Slice{{invalidMacaroon}}, - }, "test") - conn.Close() -} - // TestDeviceLogin takes a test user through the flow of logging into jimm // via the correct facades. All are done in a single test to see the flow end-2-end. // diff --git a/internal/jujuapi/api.go b/internal/jujuapi/api.go index 5530e3b12..1d31cd2e4 100644 --- a/internal/jujuapi/api.go +++ b/internal/jujuapi/api.go @@ -17,10 +17,6 @@ type Params struct { // ControllerUUID is the UUID of the JIMM controller. ControllerUUID string - // IdentityLocation holds the URL of the third-party identity - // provider. - IdentityLocation string - // PublicDNSName is the name to advertise as the public address of // the juju controller. PublicDNSName string diff --git a/internal/jujuapi/applicationoffers.go b/internal/jujuapi/applicationoffers.go index ce95ae114..245c91004 100644 --- a/internal/jujuapi/applicationoffers.go +++ b/internal/jujuapi/applicationoffers.go @@ -9,7 +9,7 @@ import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/juju/core/crossmodel" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/jimm" diff --git a/internal/jujuapi/applicationoffers_test.go b/internal/jujuapi/applicationoffers_test.go index 01ad65bb9..57d2c5204 100644 --- a/internal/jujuapi/applicationoffers_test.go +++ b/internal/jujuapi/applicationoffers_test.go @@ -5,7 +5,7 @@ import ( "context" "sort" - "github.com/juju/charm/v11" + "github.com/juju/charm/v12" "github.com/juju/juju/api/client/applicationoffers" "github.com/juju/juju/core/crossmodel" jujuparams "github.com/juju/juju/rpc/params" @@ -58,43 +58,43 @@ func (s *applicationOffersSuite) TearDownTest(c *gc.C) { } func (s *applicationOffersSuite) TestOffer(c *gc.C) { - conn := s.open(c, nil, "bob@external") + conn := s.open(c, nil, "bob@canonical.com") defer conn.Close() client := applicationoffers.NewClient(conn) - results, err := client.Offer(s.Model.UUID.String, "test-app", []string{s.endpoint.Name}, "bob@external", "test-offer", "test offer description") + results, err := client.Offer(s.Model.UUID.String, "test-app", []string{s.endpoint.Name}, "bob@canonical.com", "test-offer", "test offer description") c.Assert(err, gc.Equals, nil) c.Assert(results, gc.HasLen, 1) c.Assert(results[0].Error, gc.Equals, (*jujuparams.Error)(nil)) - results, err = client.Offer(s.Model.UUID.String, "no-such-app", []string{s.endpoint.Name}, "bob@external", "test-offer", "test offer description") + results, err = client.Offer(s.Model.UUID.String, "no-such-app", []string{s.endpoint.Name}, "bob@canonical.com", "test-offer", "test offer description") c.Assert(err, gc.Equals, nil) c.Assert(results, gc.HasLen, 1) c.Assert(results[0].Error, gc.Not(gc.IsNil)) c.Assert(results[0].Error.Code, gc.Equals, "not found") - conn1 := s.open(c, nil, "charlie@external") + conn1 := s.open(c, nil, "charlie@canonical.com") defer conn1.Close() client1 := applicationoffers.NewClient(conn1) - results, err = client1.Offer(s.Model.UUID.String, "test-app", []string{s.endpoint.Name}, "bob@external", "test-offer-2", "test offer description") + results, err = client1.Offer(s.Model.UUID.String, "test-app", []string{s.endpoint.Name}, "bob@canonical.com", "test-offer-2", "test offer description") c.Assert(err, gc.Equals, nil) c.Assert(results, gc.HasLen, 1) c.Assert(results[0].Error.Code, gc.Equals, "unauthorized access") } func (s *applicationOffersSuite) TestGetConsumeDetails(c *gc.C) { - conn := s.open(c, nil, "bob@external") + conn := s.open(c, nil, "bob@canonical.com") defer conn.Close() client := applicationoffers.NewClient(conn) - results, err := client.Offer(s.Model.UUID.String, "test-app", []string{s.endpoint.Name}, "bob@external", "test-offer", "test offer description") + results, err := client.Offer(s.Model.UUID.String, "test-app", []string{s.endpoint.Name}, "bob@canonical.com", "test-offer", "test offer description") c.Assert(err, gc.Equals, nil) c.Assert(results, gc.HasLen, 1) c.Assert(results[0].Error, gc.Equals, (*jujuparams.Error)(nil)) ourl := &crossmodel.OfferURL{ - User: "bob@external", + User: "bob@canonical.com", ModelName: "model-1", ApplicationName: "test-offer", } @@ -123,10 +123,10 @@ func (s *applicationOffersSuite) TestGetConsumeDetails(c *gc.C) { Interface: "http", }}, Users: []jujuparams.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "admin", }, { UserName: ofganames.EveryoneUser, @@ -167,10 +167,10 @@ func (s *applicationOffersSuite) TestGetConsumeDetails(c *gc.C) { Interface: "http", }}, Users: []jujuparams.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "admin", }, { UserName: ofganames.EveryoneUser, @@ -187,7 +187,7 @@ func (s *applicationOffersSuite) TestGetConsumeDetails(c *gc.C) { } func (s *applicationOffersSuite) TestListApplicationOffers(c *gc.C) { - conn := s.open(c, nil, "bob@external") + conn := s.open(c, nil, "bob@canonical.com") defer conn.Close() client := applicationoffers.NewClient(conn) @@ -195,7 +195,7 @@ func (s *applicationOffersSuite) TestListApplicationOffers(c *gc.C) { s.Model.UUID.String, "test-app", []string{s.endpoint.Name}, - "bob@external", + "bob@canonical.com", "test-offer1", "test offer 1 description", ) @@ -207,7 +207,7 @@ func (s *applicationOffersSuite) TestListApplicationOffers(c *gc.C) { s.Model.UUID.String, "test-app", []string{s.endpoint.Name}, - "bob@external", + "bob@canonical.com", "test-offer2", "test offer 2 description", ) @@ -238,17 +238,17 @@ func (s *applicationOffersSuite) TestListApplicationOffers(c *gc.C) { OfferName: "test-offer1", ApplicationName: "test-app", ApplicationDescription: "test offer 1 description", - OfferURL: "bob@external/model-1.test-offer1", + OfferURL: "bob@canonical.com/model-1.test-offer1", Endpoints: []charm.Relation{{ Name: "url", Role: "provider", Interface: "http", }}, Users: []crossmodel.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "admin", }, { UserName: ofganames.EveryoneUser, @@ -261,7 +261,7 @@ func (s *applicationOffersSuite) TestModifyOfferAccess(c *gc.C) { /* ctx := context.Background() - conn := s.open(c, nil, "bob@external") + conn := s.open(c, nil, "bob@canonical.com") defer conn.Close() client := applicationoffers.NewClient(conn) @@ -269,7 +269,7 @@ func (s *applicationOffersSuite) TestModifyOfferAccess(c *gc.C) { s.Model.UUID.String, "test-app", []string{s.endpoint.Name}, - "bob@external", + "bob@canonical.com", "test-offer1", "test offer 1 description", ) @@ -277,22 +277,22 @@ func (s *applicationOffersSuite) TestModifyOfferAccess(c *gc.C) { c.Assert(results, gc.HasLen, 1) c.Assert(results[0].Error, gc.IsNil) - offerURL := "bob@external/model-1.test-offer1" + offerURL := "bob@canonical.com/model-1.test-offer1" err = client.RevokeOffer(auth.Everyone, "read", offerURL) c.Assert(err, jc.ErrorIsNil) - err = client.GrantOffer("test.user@external", "unknown", offerURL) + err = client.GrantOffer("test.user@canonical.com", "unknown", offerURL) c.Assert(err, gc.ErrorMatches, `"unknown" offer access not valid`) - err = client.GrantOffer("test.user@external", "read", "no-such-offer") + err = client.GrantOffer("test.user@canonical.com", "read", "no-such-offer") c.Assert(err, gc.ErrorMatches, `application offer not found`) - err = client.GrantOffer("test.user@external", "admin", offerURL) + err = client.GrantOffer("test.user@canonical.com", "admin", offerURL) c.Assert(err, jc.ErrorIsNil) testUser := dbmodel.User{ - Username: "test.user@external", + Username: "test.user@canonical.com", } offer := dbmodel.ApplicationOffer{ @@ -302,7 +302,7 @@ func (s *applicationOffersSuite) TestModifyOfferAccess(c *gc.C) { c.Assert(err, jc.ErrorIsNil) c.Assert(offer.UserAccess(&testUser), gc.Equals, "admin") - err = client.RevokeOffer("test.user@external", "consume", offerURL) + err = client.RevokeOffer("test.user@canonical.com", "consume", offerURL) c.Assert(err, jc.ErrorIsNil) err = s.JIMM.Database.GetApplicationOffer(ctx, &offer) @@ -313,13 +313,13 @@ func (s *applicationOffersSuite) TestModifyOfferAccess(c *gc.C) { defer conn3.Close() client3 := applicationoffers.NewClient(conn3) - err = client3.RevokeOffer("test.user@external", "read", offerURL) + err = client3.RevokeOffer("test.user@canonical.com", "read", offerURL) c.Assert(err, gc.ErrorMatches, "unauthorized") */ } func (s *applicationOffersSuite) TestDestroyOffers(c *gc.C) { - conn := s.open(c, nil, "bob@external") + conn := s.open(c, nil, "bob@canonical.com") defer conn.Close() client := applicationoffers.NewClient(conn) @@ -327,7 +327,7 @@ func (s *applicationOffersSuite) TestDestroyOffers(c *gc.C) { s.Model.UUID.String, "test-app", []string{s.endpoint.Name}, - "bob@external", + "bob@canonical.com", "test-offer1", "test offer 1 description", ) @@ -335,29 +335,29 @@ func (s *applicationOffersSuite) TestDestroyOffers(c *gc.C) { c.Assert(results, gc.HasLen, 1) c.Assert(results[0].Error, gc.Equals, (*jujuparams.Error)(nil)) - offerURL := "bob@external/model-1.test-offer1" + offerURL := "bob@canonical.com/model-1.test-offer1" // charlie will have read access // TODO (alesstimec) until i implement proper grant/revoke access // i need to fetch the offer so that i can manually set read // permission for charlie // - //err = client.GrantOffer("charlie@external", "read", offerURL) + //err = client.GrantOffer("charlie@canonical.com", "read", offerURL) //c.Assert(err, jc.ErrorIsNil) offer := dbmodel.ApplicationOffer{ URL: offerURL, } err = s.JIMM.Database.GetApplicationOffer(context.Background(), &offer) c.Assert(err, gc.Equals, nil) - charlie := openfga.NewUser(&dbmodel.Identity{Name: "charlie@external"}, s.OFGAClient) + charlie := openfga.NewUser(&dbmodel.Identity{Name: "charlie@canonical.com"}, s.OFGAClient) err = charlie.SetApplicationOfferAccess(context.Background(), offer.ResourceTag(), ofganames.ReaderRelation) c.Assert(err, gc.Equals, nil) // try to destroy offer that does not exist - err = client.DestroyOffers(true, "bob@external/model-1.test-offer2") + err = client.DestroyOffers(true, "bob@canonical.com/model-1.test-offer2") c.Assert(err, gc.ErrorMatches, "application offer not found") - conn2 := s.open(c, nil, "charlie@external") + conn2 := s.open(c, nil, "charlie@canonical.com") defer conn2.Close() client2 := applicationoffers.NewClient(conn2) @@ -378,7 +378,7 @@ func (s *applicationOffersSuite) TestDestroyOffers(c *gc.C) { } func (s *applicationOffersSuite) TestFindApplicationOffers(c *gc.C) { - conn := s.open(c, nil, "bob@external") + conn := s.open(c, nil, "bob@canonical.com") defer conn.Close() client := applicationoffers.NewClient(conn) @@ -386,7 +386,7 @@ func (s *applicationOffersSuite) TestFindApplicationOffers(c *gc.C) { s.Model.UUID.String, "test-app", []string{s.endpoint.Name}, - "bob@external", + "bob@canonical.com", "test-offer1", "test offer 1 description", ) @@ -398,7 +398,7 @@ func (s *applicationOffersSuite) TestFindApplicationOffers(c *gc.C) { s.Model.UUID.String, "test-app", []string{s.endpoint.Name}, - "bob@external", + "bob@canonical.com", "test-offer2", "test offer 2 description", ) @@ -427,17 +427,17 @@ func (s *applicationOffersSuite) TestFindApplicationOffers(c *gc.C) { OfferName: "test-offer1", ApplicationName: "test-app", ApplicationDescription: "test offer 1 description", - OfferURL: "bob@external/model-1.test-offer1", + OfferURL: "bob@canonical.com/model-1.test-offer1", Endpoints: []charm.Relation{{ Name: "url", Role: "provider", Interface: "http", }}, Users: []crossmodel.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "admin", }, { UserName: ofganames.EveryoneUser, @@ -447,7 +447,7 @@ func (s *applicationOffersSuite) TestFindApplicationOffers(c *gc.C) { // by default each offer is publicly readable -> charlie should be // able to find it - conn2 := s.open(c, nil, "charlie@external") + conn2 := s.open(c, nil, "charlie@canonical.com") defer conn2.Close() client2 := applicationoffers.NewClient(conn2) @@ -466,7 +466,7 @@ func (s *applicationOffersSuite) TestFindApplicationOffers(c *gc.C) { OfferName: "test-offer1", ApplicationName: "test-app", ApplicationDescription: "test offer 1 description", - OfferURL: "bob@external/model-1.test-offer1", + OfferURL: "bob@canonical.com/model-1.test-offer1", Endpoints: []charm.Relation{{ Name: "url", Role: "provider", @@ -480,7 +480,7 @@ func (s *applicationOffersSuite) TestFindApplicationOffers(c *gc.C) { } func (s *applicationOffersSuite) TestApplicationOffers(c *gc.C) { - conn := s.open(c, nil, "bob@external") + conn := s.open(c, nil, "bob@canonical.com") defer conn.Close() client := applicationoffers.NewClient(conn) @@ -488,7 +488,7 @@ func (s *applicationOffersSuite) TestApplicationOffers(c *gc.C) { s.Model.UUID.String, "test-app", []string{s.endpoint.Name}, - "bob@external", + "bob@canonical.com", "test-offer1", "test offer 1 description", ) @@ -496,7 +496,7 @@ func (s *applicationOffersSuite) TestApplicationOffers(c *gc.C) { c.Assert(results, gc.HasLen, 1) c.Assert(results[0].Error, gc.Equals, (*jujuparams.Error)(nil)) - url := "bob@external/model-1.test-offer1" + url := "bob@canonical.com/model-1.test-offer1" offer, err := client.ApplicationOffer(url) c.Assert(err, jc.ErrorIsNil) @@ -509,17 +509,17 @@ func (s *applicationOffersSuite) TestApplicationOffers(c *gc.C) { OfferName: "test-offer1", ApplicationName: "test-app", ApplicationDescription: "test offer 1 description", - OfferURL: "bob@external/model-1.test-offer1", + OfferURL: "bob@canonical.com/model-1.test-offer1", Endpoints: []charm.Relation{{ Name: "url", Role: "provider", Interface: "http", }}, Users: []crossmodel.OfferUserDetails{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: "admin", }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: "admin", }, { UserName: ofganames.EveryoneUser, @@ -527,10 +527,10 @@ func (s *applicationOffersSuite) TestApplicationOffers(c *gc.C) { }}, }) - _, err = client.ApplicationOffer("charlie@external/model-1.test-offer2") + _, err = client.ApplicationOffer("charlie@canonical.com/model-1.test-offer2") c.Assert(err, gc.ErrorMatches, "application offer not found") - conn2 := s.open(c, nil, "charlie@external") + conn2 := s.open(c, nil, "charlie@canonical.com") defer conn2.Close() client2 := applicationoffers.NewClient(conn2) @@ -542,7 +542,7 @@ func (s *applicationOffersSuite) TestApplicationOffers(c *gc.C) { OfferName: "test-offer1", ApplicationName: "test-app", ApplicationDescription: "test offer 1 description", - OfferURL: "bob@external/model-1.test-offer1", + OfferURL: "bob@canonical.com/model-1.test-offer1", Endpoints: []charm.Relation{{ Name: "url", Role: "provider", diff --git a/internal/jujuapi/cloud.go b/internal/jujuapi/cloud.go index ba3db7a90..39c1f31c6 100644 --- a/internal/jujuapi/cloud.go +++ b/internal/jujuapi/cloud.go @@ -10,7 +10,7 @@ import ( apiservererrors "github.com/juju/juju/apiserver/errors" "github.com/juju/juju/cloud" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" diff --git a/internal/jujuapi/cloud_test.go b/internal/jujuapi/cloud_test.go index 6105a414f..41990d849 100644 --- a/internal/jujuapi/cloud_test.go +++ b/internal/jujuapi/cloud_test.go @@ -11,7 +11,7 @@ import ( "github.com/juju/juju/api/client/modelmanager" "github.com/juju/juju/cloud" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" @@ -78,10 +78,10 @@ func (s *cloudSuite) TestUserCredentials(c *gc.C) { conn := s.open(c, nil, "bob") defer conn.Close() client := cloudapi.NewClient(conn) - creds, err := client.UserCredentials(names.NewUserTag("bob@external"), names.NewCloudTag(jimmtest.TestCloudName)) + creds, err := client.UserCredentials(names.NewUserTag("bob@canonical.com"), names.NewCloudTag(jimmtest.TestCloudName)) c.Assert(err, gc.Equals, nil) c.Assert(creds, jc.DeepEquals, []names.CloudCredentialTag{ - names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@external/cred"), + names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@canonical.com/cred"), }) } @@ -124,7 +124,7 @@ func (s *cloudSuite) TestUpdateCloudCredentials(c *gc.C) { conn := s.open(c, nil, "test") defer conn.Close() client := cloudapi.NewClient(conn) - credentialTag := names.NewCloudCredentialTag(fmt.Sprintf(jimmtest.TestCloudName + "/test@external/cred3")) + credentialTag := names.NewCloudCredentialTag(fmt.Sprintf(jimmtest.TestCloudName + "/test@canonical.com/cred3")) reqCreds := map[string]cloud.Credential{ credentialTag.String(): cloud.NewCredential("credtype", map[string]string{ "attr1": "val31", @@ -136,12 +136,12 @@ func (s *cloudSuite) TestUpdateCloudCredentials(c *gc.C) { c.Assert(res, gc.DeepEquals, []jujuparams.UpdateCredentialResult{{ CredentialTag: credentialTag.String(), }}) - creds, err := client.UserCredentials(names.NewUserTag("test@external"), names.NewCloudTag(jimmtest.TestCloudName)) + creds, err := client.UserCredentials(names.NewUserTag("test@canonical.com"), names.NewCloudTag(jimmtest.TestCloudName)) c.Assert(err, gc.Equals, nil) c.Assert(creds, jc.DeepEquals, []names.CloudCredentialTag{credentialTag}) _, err = client.UpdateCredentialsCheckModels(credentialTag, cloud.NewCredential("credtype", map[string]string{"attr1": "val33", "attr2": "val34"})) c.Assert(err, gc.Equals, nil) - creds, err = client.UserCredentials(names.NewUserTag("test@external"), names.NewCloudTag(jimmtest.TestCloudName)) + creds, err = client.UserCredentials(names.NewUserTag("test@canonical.com"), names.NewCloudTag(jimmtest.TestCloudName)) c.Assert(err, gc.Equals, nil) var _ = creds } @@ -159,7 +159,7 @@ func (s *cloudSuite) TestUpdateCloudCredentialsErrors(c *gc.C) { }, }, }, { - Tag: names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test2@external/cred1").String(), + Tag: names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test2@canonical.com/cred1").String(), Credential: jujuparams.CloudCredential{ AuthType: "credtype", Attributes: map[string]string{ @@ -167,7 +167,7 @@ func (s *cloudSuite) TestUpdateCloudCredentialsErrors(c *gc.C) { }, }, }, { - Tag: names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@external/bad-name-").String(), + Tag: names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@canonical.com/bad-name-").String(), Credential: jujuparams.CloudCredential{ AuthType: "credtype", Attributes: map[string]string{ @@ -189,12 +189,12 @@ func (s *cloudSuite) TestUpdateCloudCredentialsForce(c *gc.C) { conn := s.open(c, nil, "test") defer conn.Close() client := cloudapi.NewClient(conn) - credentialTag := names.NewCloudCredentialTag(fmt.Sprintf(jimmtest.TestCloudName + "/test@external/cred3")) + credentialTag := names.NewCloudCredentialTag(fmt.Sprintf(jimmtest.TestCloudName + "/test@canonical.com/cred3")) _, err := client.UpdateCredentialsCheckModels(credentialTag, cloud.NewCredential("userpass", map[string]string{"username": "a", "password": "b"})) c.Assert(err, gc.Equals, nil) mmclient := modelmanager.NewClient(conn) - _, err = mmclient.CreateModel("model1", "test@external", jimmtest.TestCloudName, "", credentialTag, nil) + _, err = mmclient.CreateModel("model1", "test@canonical.com", jimmtest.TestCloudName, "", credentialTag, nil) c.Assert(err, gc.Equals, nil) args := jujuparams.UpdateCredentialArgs{ @@ -233,7 +233,7 @@ func (s *cloudSuite) TestUpdateCloudCredentialsForce(c *gc.C) { args.Force = true err = conn.APICall("Cloud", 7, "", "UpdateCredentialsCheckModels", args, &resp) c.Assert(err, gc.Equals, nil) - c.Check(resp.Results[0].Error, gc.ErrorMatches, `updating cloud credentials: validating credential "`+jimmtest.TestCloudName+`/test@external/cred3" for cloud "`+jimmtest.TestCloudName+`": supported auth-types \["empty" "userpass"\], "badauthtype" not supported`) + c.Check(resp.Results[0].Error, gc.ErrorMatches, `updating cloud credentials: validating credential "`+jimmtest.TestCloudName+`/test@canonical.com/cred3" for cloud "`+jimmtest.TestCloudName+`": supported auth-types \["empty" "userpass"\], "badauthtype" not supported`) // Check that the credentials have been updated even though // we got an error. @@ -252,7 +252,7 @@ func (s *cloudSuite) TestCheckCredentialsModels(c *gc.C) { conn := s.open(c, nil, "test") defer conn.Close() - credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@external/cred") + credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@canonical.com/cred") cred1 := cloud.NewCredential("userpass", map[string]string{ "username": "cloud-user", "password": "cloud-pass", @@ -263,10 +263,10 @@ func (s *cloudSuite) TestCheckCredentialsModels(c *gc.C) { c.Assert(err, gc.Equals, nil) mmclient := modelmanager.NewClient(conn) - model1, err := mmclient.CreateModel("model1", "test@external", jimmtest.TestCloudName, "", credTag, nil) + model1, err := mmclient.CreateModel("model1", "test@canonical.com", jimmtest.TestCloudName, "", credTag, nil) c.Assert(err, gc.Equals, nil) - model2, err := mmclient.CreateModel("model2", "test@external", jimmtest.TestCloudName, "", credTag, nil) + model2, err := mmclient.CreateModel("model2", "test@canonical.com", jimmtest.TestCloudName, "", credTag, nil) c.Assert(err, gc.Equals, nil) var resp jujuparams.UpdateCredentialResults @@ -305,7 +305,7 @@ func (s *cloudSuite) TestCheckCredentialsModelsInvalidCreds(c *gc.C) { conn := s.open(c, nil, "test") defer conn.Close() - credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@external/cred") + credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@canonical.com/cred") cred1 := cloud.NewCredential("userpass", map[string]string{ "username": "cloud-user", "password": "cloud-pass", @@ -316,7 +316,7 @@ func (s *cloudSuite) TestCheckCredentialsModelsInvalidCreds(c *gc.C) { c.Assert(err, gc.Equals, nil) mmclient := modelmanager.NewClient(conn) - model1, err := mmclient.CreateModel("model1", "test@external", jimmtest.TestCloudName, "", credTag, nil) + model1, err := mmclient.CreateModel("model1", "test@canonical.com", jimmtest.TestCloudName, "", credTag, nil) c.Assert(err, gc.Equals, nil) var resp jujuparams.UpdateCredentialResults @@ -334,14 +334,14 @@ func (s *cloudSuite) TestCheckCredentialsModelsInvalidCreds(c *gc.C) { c.Assert(err, gc.Equals, nil) c.Assert(resp, jc.DeepEquals, jujuparams.UpdateCredentialResults{ Results: []jujuparams.UpdateCredentialResult{{ - CredentialTag: "cloudcred-" + jimmtest.TestCloudName + "_test@external_cred", + CredentialTag: "cloudcred-" + jimmtest.TestCloudName + "_test@canonical.com_cred", Error: &jujuparams.Error{Message: "some models are no longer visible"}, Models: []jujuparams.UpdateCredentialModelResult{{ ModelUUID: model1.UUID, ModelName: "model1", Errors: []jujuparams.ErrorResult{{ Error: &jujuparams.Error{ - Message: `validating credential "` + jimmtest.TestCloudName + `/test@external/cred" for cloud "` + jimmtest.TestCloudName + `": supported auth-types ["empty" "userpass"], "unknowntype" not supported`, + Message: `validating credential "` + jimmtest.TestCloudName + `/test@canonical.com/cred" for cloud "` + jimmtest.TestCloudName + `": supported auth-types ["empty" "userpass"], "unknowntype" not supported`, Code: "not supported", }, }}, @@ -354,12 +354,12 @@ func (s *cloudSuite) TestCredential(c *gc.C) { conn := s.open(c, nil, "test") defer conn.Close() - cred1Tag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@external/cred1") + cred1Tag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@canonical.com/cred1") cred1 := cloud.NewCredential("userpass", map[string]string{ "username": "cloud-user", "password": "cloud-pass", }) - cred2Tag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@external/cred2") + cred2Tag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@canonical.com/cred2") cred2 := cloud.NewCredential("empty", nil) client := cloudapi.NewClient(conn) @@ -371,8 +371,8 @@ func (s *cloudSuite) TestCredential(c *gc.C) { creds, err := client.Credentials( cred1Tag, cred2Tag, - names.NewCloudCredentialTag(jimmtest.TestCloudName+"/test@external/cred3"), - names.NewCloudCredentialTag(jimmtest.TestCloudName+"/no-test@external/cred4"), + names.NewCloudCredentialTag(jimmtest.TestCloudName+"/test@canonical.com/cred3"), + names.NewCloudCredentialTag(jimmtest.TestCloudName+"/no-test@canonical.com/cred4"), names.NewCloudCredentialTag(jimmtest.TestCloudName+"/admin@local/cred6"), ) c.Assert(err, gc.Equals, nil) @@ -398,7 +398,7 @@ func (s *cloudSuite) TestCredential(c *gc.C) { }, }, { Error: &jujuparams.Error{ - Message: `cloudcredential "` + jimmtest.TestCloudName + `/test@external/cred3" not found`, + Message: `cloudcredential "` + jimmtest.TestCloudName + `/test@canonical.com/cred3" not found`, Code: jujuparams.CodeNotFound, }, }, { @@ -418,7 +418,7 @@ func (s *cloudSuite) TestRevokeCredential(c *gc.C) { conn := s.open(c, nil, "test") defer conn.Close() client := cloudapi.NewClient(conn) - credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@external/cred") + credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@canonical.com/cred") _, err := client.UpdateCredentialsCheckModels( credTag, cloud.NewCredential("empty", nil), @@ -447,7 +447,7 @@ func (s *cloudSuite) TestRevokeCredential(c *gc.C) { c.Assert(ccr, jc.DeepEquals, []jujuparams.CloudCredentialResult{{ Error: &jujuparams.Error{ Code: jujuparams.CodeNotFound, - Message: `cloudcredential "` + jimmtest.TestCloudName + `/test@external/cred" not found`, + Message: `cloudcredential "` + jimmtest.TestCloudName + `/test@canonical.com/cred" not found`, }, }}) @@ -489,7 +489,7 @@ func (s *cloudSuite) TestRevokeCredentialsCheckModels(c *gc.C) { conn := s.open(c, nil, "test") defer conn.Close() client := cloudapi.NewClient(conn) - credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@external/cred") + credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@canonical.com/cred") _, err := client.UpdateCredentialsCheckModels( credTag, cloud.NewCredential("empty", nil), @@ -511,7 +511,7 @@ func (s *cloudSuite) TestRevokeCredentialsCheckModels(c *gc.C) { }}) mmclient := modelmanager.NewClient(conn) - _, err = mmclient.CreateModel("test", "test@external", jimmtest.TestCloudName, jimmtest.TestCloudRegionName, credTag, nil) + _, err = mmclient.CreateModel("test", "test@canonical.com", jimmtest.TestCloudName, jimmtest.TestCloudRegionName, credTag, nil) c.Assert(err, gc.Equals, nil) var resp jujuparams.ErrorResults @@ -539,7 +539,7 @@ func (s *cloudSuite) TestRevokeCredentialsCheckModels(c *gc.C) { c.Assert(ccr, jc.DeepEquals, []jujuparams.CloudCredentialResult{{ Error: &jujuparams.Error{ Code: jujuparams.CodeNotFound, - Message: `cloudcredential "` + jimmtest.TestCloudName + `/test@external/cred" not found`, + Message: `cloudcredential "` + jimmtest.TestCloudName + `/test@canonical.com/cred" not found`, }, }}) @@ -597,7 +597,7 @@ func (s *cloudSuite) TestAddCredential(c *gc.C) { conn := s.open(c, nil, "test") defer conn.Close() client := cloudapi.NewClient(conn) - credentialTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@external/cred3") + credentialTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@canonical.com/cred3") err := client.AddCredential( credentialTag.String(), cloud.NewCredential( @@ -656,7 +656,7 @@ func (s *cloudSuite) TestCredentialContents(c *gc.C) { conn := s.open(c, nil, "test") defer conn.Close() client := cloudapi.NewClient(conn) - credentialTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@external/cred3") + credentialTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@canonical.com/cred3") err := client.AddCredential( credentialTag.String(), cloud.NewCredential( @@ -684,7 +684,7 @@ func (s *cloudSuite) TestCredentialContents(c *gc.C) { }}) mmclient := modelmanager.NewClient(conn) - _, err = mmclient.CreateModel("model1", "test@external", jimmtest.TestCloudName, "", credentialTag, nil) + _, err = mmclient.CreateModel("model1", "test@canonical.com", jimmtest.TestCloudName, "", credentialTag, nil) c.Assert(err, gc.Equals, nil) creds, err = client.CredentialContents(jimmtest.TestCloudName, "cred3", true) @@ -792,7 +792,7 @@ func (s *cloudSuite) TestModifyCloudAccess(c *gc.C) { _, ok := clouds[names.NewCloudTag("test-cloud")] c.Assert(ok, jc.IsTrue) - // Check that bob@external does not yet have access + // Check that bob@canonical.com does not yet have access conn2 := s.open(c, nil, "bob") defer conn2.Close() client2 := cloudapi.NewClient(conn2) @@ -801,7 +801,7 @@ func (s *cloudSuite) TestModifyCloudAccess(c *gc.C) { _, ok = clouds[names.NewCloudTag("test-cloud")] c.Assert(ok, gc.Equals, false, gc.Commentf("clouds: %#v", clouds)) - err = client.GrantCloud("bob@external", "add-model", "test-cloud") + err = client.GrantCloud("bob@canonical.com", "add-model", "test-cloud") c.Assert(err, gc.Equals, nil) clouds, err = client2.Clouds() @@ -809,7 +809,7 @@ func (s *cloudSuite) TestModifyCloudAccess(c *gc.C) { _, ok = clouds[names.NewCloudTag("test-cloud")] c.Assert(ok, jc.IsTrue) - err = client.RevokeCloud("bob@external", "add-model", "test-cloud") + err = client.RevokeCloud("bob@canonical.com", "add-model", "test-cloud") c.Assert(err, gc.Equals, nil) clouds, err = client2.Clouds() c.Assert(err, gc.Equals, nil) @@ -840,7 +840,7 @@ func (s *cloudSuite) TestModifyCloudAccessUnauthorized(c *gc.C) { conn2 := s.open(c, nil, "charlie") defer conn2.Close() client2 := cloudapi.NewClient(conn2) - err = client2.GrantCloud("charlie@external", "add-model", "test-cloud") + err = client2.GrantCloud("charlie@canonical.com", "add-model", "test-cloud") c.Assert(err, gc.ErrorMatches, `unauthorized`) } @@ -943,21 +943,21 @@ func (s *cloudSuite) TestListCloudInfo(c *gc.C) { c.Assert(err, gc.Equals, nil) /* - err = client.GrantCloud("bob@external", "add-model", "test-cloud") + err = client.GrantCloud("bob@canonical.com", "add-model", "test-cloud") c.Assert(err, gc.Equals, nil) */ - bob := openfga.NewUser(&dbmodel.Identity{Name: "bob@external"}, s.OFGAClient) + bob := openfga.NewUser(&dbmodel.Identity{Name: "bob@canonical.com"}, s.OFGAClient) err = bob.SetCloudAccess(context.Background(), names.NewCloudTag("test-cloud"), ofganames.CanAddModelRelation) c.Assert(err, gc.Equals, nil) err = bob.SetCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), ofganames.CanAddModelRelation) c.Assert(err, gc.Equals, nil) - alice := openfga.NewUser(&dbmodel.Identity{Name: "alice@external"}, s.OFGAClient) + alice := openfga.NewUser(&dbmodel.Identity{Name: "alice@canonical.com"}, s.OFGAClient) err = alice.SetCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), ofganames.CanAddModelRelation) c.Assert(err, gc.Equals, nil) args := jujuparams.ListCloudsRequest{ - UserTag: names.NewUserTag("alice@external").String(), + UserTag: names.NewUserTag("alice@canonical.com").String(), All: false, } var result jujuparams.ListCloudInfoResults @@ -1005,7 +1005,7 @@ func (s *cloudSuite) TestListCloudInfo(c *gc.C) { defer conn.Close() args = jujuparams.ListCloudsRequest{ - UserTag: names.NewUserTag("bob@external").String(), + UserTag: names.NewUserTag("bob@canonical.com").String(), All: false, } result.Results = nil diff --git a/internal/jujuapi/controller.go b/internal/jujuapi/controller.go index 9db1e2299..6af5964b0 100644 --- a/internal/jujuapi/controller.go +++ b/internal/jujuapi/controller.go @@ -7,7 +7,7 @@ import ( "fmt" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" @@ -74,9 +74,7 @@ func (r *controllerRoot) MongoVersion(ctx context.Context) (jujuparams.StringRes // provider for this controller or an empty string if no external identity // provider has been configured when the controller was bootstrapped. func (r *controllerRoot) IdentityProviderURL(ctx context.Context) (jujuparams.StringResult, error) { - return jujuparams.StringResult{ - Result: r.params.IdentityLocation, - }, nil + return jujuparams.StringResult{Result: ""}, nil } // ControllerVersion returns the version information associated with this diff --git a/internal/jujuapi/controller_test.go b/internal/jujuapi/controller_test.go index 3ba261877..56833cd73 100644 --- a/internal/jujuapi/controller_test.go +++ b/internal/jujuapi/controller_test.go @@ -17,7 +17,7 @@ import ( "github.com/juju/juju/core/life" jujuparams "github.com/juju/juju/rpc/params" jujuversion "github.com/juju/juju/version" - "github.com/juju/names/v4" + "github.com/juju/names/v5" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" @@ -101,13 +101,13 @@ func (s *controllerSuite) TestAllModels(c *gc.C) { c.Assert(models, jc.SameContents, []base.UserModel{{ Name: "model-1", UUID: s.Model.UUID.String, - Owner: "bob@external", + Owner: "bob@canonical.com", LastConnection: nil, Type: "iaas", }, { Name: "model-3", UUID: s.Model3.UUID.String, - Owner: "charlie@external", + Owner: "charlie@canonical.com", LastConnection: nil, Type: "iaas", }}) @@ -124,7 +124,7 @@ func (s *controllerSuite) TestModelStatus(c *gc.C) { c.Check(models[0], jc.DeepEquals, base.ModelStatus{ UUID: s.Model.UUID.String, Life: life.Value(constants.ALIVE.String()), - Owner: "bob@external", + Owner: "bob@canonical.com", TotalMachineCount: 0, CoreCount: 0, HostedMachineCount: 0, @@ -166,7 +166,7 @@ func (s *controllerSuite) TestIdentityProviderURL(c *gc.C) { var result jujuparams.StringResult err := conn.APICall("Controller", 11, "", "IdentityProviderURL", nil, &result) c.Assert(err, jc.ErrorIsNil) - c.Assert(result.Result, gc.Matches, `https://127\.0\.0\.1.*`) + c.Assert(result.Result, gc.Matches, ``) } func (s *controllerSuite) TestControllerVersion(c *gc.C) { @@ -187,11 +187,11 @@ func (s *controllerSuite) TestControllerAccess(c *gc.C) { defer conn.Close() client := controllerapi.NewClient(conn) - access, err := client.GetControllerAccess("alice@external") + access, err := client.GetControllerAccess("alice@canonical.com") c.Assert(err, gc.Equals, nil) c.Check(string(access), gc.Equals, "superuser") - access, err = client.GetControllerAccess("bob@external") + access, err = client.GetControllerAccess("bob@canonical.com") c.Assert(err, gc.Equals, nil) c.Check(string(access), gc.Equals, "login") @@ -199,11 +199,11 @@ func (s *controllerSuite) TestControllerAccess(c *gc.C) { defer conn.Close() client = controllerapi.NewClient(conn) - access, err = client.GetControllerAccess("bob@external") + access, err = client.GetControllerAccess("bob@canonical.com") c.Assert(err, gc.Equals, nil) c.Check(string(access), gc.Equals, "login") - _, err = client.GetControllerAccess("alice@external") + _, err = client.GetControllerAccess("alice@canonical.com") c.Assert(err, gc.ErrorMatches, `unauthorized`) } diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index e87fb8aa9..883cd9881 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -9,7 +9,7 @@ import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/version" "github.com/rogpeppe/fastuuid" "golang.org/x/oauth2" @@ -34,7 +34,6 @@ type JIMM interface { AddGroup(ctx context.Context, user *openfga.User, name string) error AddModel(ctx context.Context, u *openfga.User, args *jimm.ModelCreateArgs) (_ *jujuparams.ModelInfo, err error) AddServiceAccount(ctx context.Context, u *openfga.User, clientId string) error - Authenticate(ctx context.Context, req *jujuparams.LoginRequest) (*openfga.User, error) OAuthAuthenticationService() jimm.OAuthAuthenticator AuthorizationClient() *openfga.OFGAClient ChangeModelCredential(ctx context.Context, user *openfga.User, modelTag names.ModelTag, cloudCredentialTag names.CloudCredentialTag) error diff --git a/internal/jujuapi/jimm.go b/internal/jujuapi/jimm.go index b49da379b..60be3cc56 100644 --- a/internal/jujuapi/jimm.go +++ b/internal/jujuapi/jimm.go @@ -11,7 +11,7 @@ import ( "github.com/juju/juju/core/network" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil" "github.com/juju/zaputil/zapctx" diff --git a/internal/jujuapi/jimm_test.go b/internal/jujuapi/jimm_test.go index 08b1b62e0..9d9014e2f 100644 --- a/internal/jujuapi/jimm_test.go +++ b/internal/jujuapi/jimm_test.go @@ -13,7 +13,7 @@ import ( "github.com/juju/juju/cloud" jujuparams "github.com/juju/juju/rpc/params" jujuversion "github.com/juju/juju/version" - "github.com/juju/names/v4" + "github.com/juju/names/v5" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" @@ -306,9 +306,9 @@ func (s *jimmSuite) TestAuditLog(c *gc.C) { evs, err := client2.FindAuditEvents(&apiparams.FindAuditEventsRequest{}) c.Assert(err, gc.Equals, nil) - c.Assert(len(evs.Events), gc.Equals, 13) + c.Assert(len(evs.Events), gc.Equals, 9) - bobTag := names.NewUserTag("bob@external").String() + bobTag := names.NewUserTag("bob@canonical.com").String() expectedEvents := apiparams.AuditEvents{ Events: []apiparams.AuditEvent{{ @@ -316,7 +316,7 @@ func (s *jimmSuite) TestAuditLog(c *gc.C) { ConversationId: evs.Events[0].ConversationId, MessageId: 1, FacadeName: "Admin", - FacadeMethod: "Login", + FacadeMethod: "LoginWithSessionToken", FacadeVersion: evs.Events[0].FacadeVersion, ObjectId: "", UserTag: "user-", @@ -328,10 +328,10 @@ func (s *jimmSuite) TestAuditLog(c *gc.C) { ConversationId: evs.Events[1].ConversationId, MessageId: 1, FacadeName: "Admin", - FacadeMethod: "Login", + FacadeMethod: "LoginWithSessionToken", FacadeVersion: evs.Events[1].FacadeVersion, ObjectId: "", - UserTag: "user-", + UserTag: bobTag, IsResponse: true, Params: nil, Errors: evs.Events[1].Errors, @@ -339,11 +339,11 @@ func (s *jimmSuite) TestAuditLog(c *gc.C) { Time: evs.Events[2].Time, ConversationId: evs.Events[2].ConversationId, MessageId: 2, - FacadeName: "Admin", - FacadeMethod: "Login", + FacadeName: "JIMM", + FacadeMethod: "FindAuditEvents", FacadeVersion: evs.Events[2].FacadeVersion, ObjectId: "", - UserTag: "user-", + UserTag: bobTag, IsResponse: false, Params: evs.Events[2].Params, Errors: nil, @@ -351,8 +351,8 @@ func (s *jimmSuite) TestAuditLog(c *gc.C) { Time: evs.Events[3].Time, ConversationId: evs.Events[3].ConversationId, MessageId: 2, - FacadeName: "Admin", - FacadeMethod: "Login", + FacadeName: "JIMM", + FacadeMethod: "FindAuditEvents", FacadeVersion: evs.Events[3].FacadeVersion, ObjectId: "", UserTag: bobTag, @@ -368,7 +368,7 @@ func (s *jimmSuite) TestAuditLog(c *gc.C) { // alice can grant bob access to audit log entries err = client2.GrantAuditLogAccess(&apiparams.AuditLogAccessRequest{ - UserTag: names.NewUserTag("bob@external").String(), + UserTag: names.NewUserTag("bob@canonical.com").String(), }) c.Assert(err, gc.Equals, nil) @@ -456,7 +456,7 @@ func TestAuditLogAPIParamsConversion(t *testing.T) { func (s *jimmSuite) TestFullModelStatus(c *gc.C) { s.AddController(c, "controller-2", s.APIInfo(c)) - mt := s.AddModel(c, names.NewUserTag("charlie@external"), "model-1", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, s.Model2.CloudCredential.ResourceTag()) + mt := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-1", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, s.Model2.CloudCredential.ResourceTag()) conn := s.open(c, nil, "bob") defer conn.Close() @@ -472,7 +472,7 @@ func (s *jimmSuite) TestFullModelStatus(c *gc.C) { }) c.Assert(err, gc.ErrorMatches, "unauthorized.*") - conn = s.open(c, nil, "alice@external") + conn = s.open(c, nil, "alice@canonical.com") defer conn.Close() client = api.NewClient(conn) @@ -568,12 +568,12 @@ func (s *jimmSuite) TestImportModel(c *gc.C) { func (s *jimmSuite) TestAddCloudToController(c *gc.C) { ctx := context.Background() u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } err := s.JIMM.Database.GetIdentity(ctx, &u) c.Assert(err, gc.IsNil) - conn := s.open(c, nil, "alice@external") + conn := s.open(c, nil, "alice@canonical.com") defer conn.Close() req := apiparams.AddCloudToControllerRequest{ @@ -605,12 +605,12 @@ func (s *jimmSuite) TestAddCloudToController(c *gc.C) { func (s *jimmSuite) TestAddExistingCloudToController(c *gc.C) { ctx := context.Background() u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } err := s.JIMM.Database.GetIdentity(ctx, &u) c.Assert(err, gc.IsNil) - conn := s.open(c, nil, "alice@external") + conn := s.open(c, nil, "alice@canonical.com") defer conn.Close() force := true @@ -653,12 +653,12 @@ func (s *jimmSuite) TestAddExistingCloudToController(c *gc.C) { func (s *jimmSuite) TestRemoveCloudFromController(c *gc.C) { ctx := context.Background() u := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } err := s.JIMM.Database.GetIdentity(ctx, &u) c.Assert(err, gc.IsNil) - conn := s.open(c, nil, "alice@external") + conn := s.open(c, nil, "alice@canonical.com") defer conn.Close() req := apiparams.AddCloudToControllerRequest{ @@ -699,7 +699,7 @@ func (s *jimmSuite) TestCrossModelQuery(c *gc.C) { s.AddController(c, "controller-2", s.APIInfo(c)) s.AddModel( c, - names.NewUserTag("charlie@external"), + names.NewUserTag("charlie@canonical.com"), "model-20", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, @@ -707,7 +707,7 @@ func (s *jimmSuite) TestCrossModelQuery(c *gc.C) { ) s.AddModel( c, - names.NewUserTag("charlie@external"), + names.NewUserTag("charlie@canonical.com"), "model-21", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, @@ -715,7 +715,7 @@ func (s *jimmSuite) TestCrossModelQuery(c *gc.C) { ) s.AddModel( c, - names.NewUserTag("charlie@external"), + names.NewUserTag("charlie@canonical.com"), "model-22", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, @@ -774,7 +774,7 @@ func (s *jimmSuite) TestCrossModelQuery(c *gc.C) { func (s *jimmSuite) TestJimmModelMigrationSuperuser(c *gc.C) { mt := s.AddModel( c, - names.NewUserTag("charlie@external"), + names.NewUserTag("charlie@canonical.com"), "model-20", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, @@ -801,7 +801,7 @@ func (s *jimmSuite) TestJimmModelMigrationSuperuser(c *gc.C) { func (s *jimmSuite) TestJimmModelMigrationNonSuperuser(c *gc.C) { mt := s.AddModel( c, - names.NewUserTag("charlie@external"), + names.NewUserTag("charlie@canonical.com"), "model-20", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, diff --git a/internal/jujuapi/modelmanager.go b/internal/jujuapi/modelmanager.go index 31f9aef39..b012fae61 100644 --- a/internal/jujuapi/modelmanager.go +++ b/internal/jujuapi/modelmanager.go @@ -7,7 +7,7 @@ import ( "fmt" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" diff --git a/internal/jujuapi/modelmanager_test.go b/internal/jujuapi/modelmanager_test.go index 35667e163..640dccaba 100644 --- a/internal/jujuapi/modelmanager_test.go +++ b/internal/jujuapi/modelmanager_test.go @@ -21,7 +21,7 @@ import ( "github.com/juju/juju/state" "github.com/juju/juju/testing/factory" jujuversion "github.com/juju/juju/version" - "github.com/juju/names/v4" + "github.com/juju/names/v5" jc "github.com/juju/testing/checkers" "github.com/juju/utils/v2" gc "gopkg.in/check.v1" @@ -34,7 +34,7 @@ import ( ofganames "github.com/canonical/jimm/internal/openfga/names" ) -const jujuVersion = "3.3.1" +const jujuVersion = "3.5-beta1" type modelManagerSuite struct { websocketSuite @@ -55,7 +55,7 @@ func (s *modelManagerSuite) TestListModelSummaries(c *gc.C) { c.Assert(err, gc.Equals, nil) client := modelmanager.NewClient(conn) - models, err := client.ListModelSummaries("bob@external", false) + models, err := client.ListModelSummaries("bob@canonical.com", false) c.Assert(err, gc.Equals, nil) c.Assert(models, jimmtest.CmpEquals( cmpopts.IgnoreTypes(&time.Time{}), @@ -70,8 +70,8 @@ func (s *modelManagerSuite) TestListModelSummaries(c *gc.C) { DefaultSeries: "jammy", Cloud: jimmtest.TestCloudName, CloudRegion: jimmtest.TestCloudRegionName, - CloudCredential: jimmtest.TestCloudName + "/bob@external/cred", - Owner: "bob@external", + CloudCredential: jimmtest.TestCloudName + "/bob@canonical.com/cred", + Owner: "bob@canonical.com", Life: life.Value(constants.ALIVE.String()), Status: base.Status{ Status: status.Available, @@ -101,8 +101,8 @@ func (s *modelManagerSuite) TestListModelSummaries(c *gc.C) { DefaultSeries: "jammy", Cloud: jimmtest.TestCloudName, CloudRegion: jimmtest.TestCloudRegionName, - CloudCredential: jimmtest.TestCloudName + "/charlie@external/cred", - Owner: "charlie@external", + CloudCredential: jimmtest.TestCloudName + "/charlie@canonical.com/cred", + Owner: "charlie@canonical.com", Life: life.Value(constants.ALIVE.String()), Status: base.Status{ Status: status.Available, @@ -133,12 +133,12 @@ func (s *modelManagerSuite) TestListModelSummariesWithoutControllerUUIDMasking(c err := conn1.APICall("JIMM", 4, "", "DisableControllerUUIDMasking", nil, nil) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) - s.Candid.AddUser("adam", "controller-admin") + s.AddAdminUser(c, "adam@canonical.com") // we need to make bob jimm admin to disable controller UUID masking err = s.OFGAClient.AddRelation(context.Background(), openfga.Tuple{ - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(s.JIMM.ResourceTag()), }, @@ -155,7 +155,7 @@ func (s *modelManagerSuite) TestListModelSummariesWithoutControllerUUIDMasking(c // connection, we can make bob a regular user again. err = s.OFGAClient.RemoveRelation(context.Background(), openfga.Tuple{ - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(s.JIMM.ResourceTag()), }, @@ -178,8 +178,8 @@ func (s *modelManagerSuite) TestListModelSummariesWithoutControllerUUIDMasking(c DefaultSeries: "jammy", Cloud: jimmtest.TestCloudName, CloudRegion: jimmtest.TestCloudRegionName, - CloudCredential: jimmtest.TestCloudName + "/bob@external/cred", - Owner: "bob@external", + CloudCredential: jimmtest.TestCloudName + "/bob@canonical.com/cred", + Owner: "bob@canonical.com", Life: life.Value(constants.ALIVE.String()), Status: base.Status{ Status: status.Available, @@ -209,8 +209,8 @@ func (s *modelManagerSuite) TestListModelSummariesWithoutControllerUUIDMasking(c DefaultSeries: "jammy", Cloud: jimmtest.TestCloudName, CloudRegion: jimmtest.TestCloudRegionName, - CloudCredential: jimmtest.TestCloudName + "/charlie@external/cred", - Owner: "charlie@external", + CloudCredential: jimmtest.TestCloudName + "/charlie@canonical.com/cred", + Owner: "charlie@canonical.com", Life: life.Value(constants.ALIVE.String()), Status: base.Status{ Status: status.Available, @@ -245,32 +245,32 @@ func (s *modelManagerSuite) TestListModels(c *gc.C) { c.Assert(models, jc.SameContents, []base.UserModel{{ Name: "model-1", UUID: s.Model.UUID.String, - Owner: "bob@external", + Owner: "bob@canonical.com", Type: "iaas", }, { Name: "model-3", UUID: s.Model3.UUID.String, - Owner: "charlie@external", + Owner: "charlie@canonical.com", Type: "iaas", }}) } func (s *modelManagerSuite) TestModelInfo(c *gc.C) { - mt4 := s.AddModel(c, names.NewUserTag("charlie@external"), "model-4", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, s.Model2.CloudCredential.ResourceTag()) + mt4 := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-4", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, s.Model2.CloudCredential.ResourceTag()) // TODO (alesstimec) change once granting has been re-implemented //conn := s.open(c, nil, "charlie") //defer conn.Close() //client := modelmanager.NewClient(conn) - //err := client.GrantModel("bob@external", "write", mt4.Id()) + //err := client.GrantModel("bob@canonical.com", "write", mt4.Id()) //c.Assert(err, gc.Equals, nil) - bob := openfga.NewUser(&dbmodel.Identity{Name: "bob@external"}, s.OFGAClient) + bob := openfga.NewUser(&dbmodel.Identity{Name: "bob@canonical.com"}, s.OFGAClient) err := bob.SetModelAccess(context.Background(), mt4, ofganames.WriterRelation) c.Assert(err, gc.Equals, nil) - mt5 := s.AddModel(c, names.NewUserTag("charlie@external"), "model-5", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, s.Model2.CloudCredential.ResourceTag()) + mt5 := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-5", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, s.Model2.CloudCredential.ResourceTag()) // TODO (alesstimec) change once granting has been re-implemented - //err = client.GrantModel("bob@external", "admin", mt5.Id()) + //err = client.GrantModel("bob@canonical.com", "admin", mt5.Id()) //c.Assert(err, gc.Equals, nil) err = bob.SetModelAccess(context.Background(), mt5, ofganames.AdministratorRelation) c.Assert(err, gc.Equals, nil) @@ -333,7 +333,7 @@ func (s *modelManagerSuite) TestModelInfo(c *gc.C) { CloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), CloudRegion: jimmtest.TestCloudRegionName, CloudCredentialTag: s.Model.CloudCredential.Tag().String(), - OwnerTag: names.NewUserTag("bob@external").String(), + OwnerTag: names.NewUserTag("bob@canonical.com").String(), Life: life.Alive, Status: jujuparams.EntityStatus{ Status: status.Available, @@ -342,10 +342,10 @@ func (s *modelManagerSuite) TestModelInfo(c *gc.C) { Level: "unsupported", }, Users: []jujuparams.ModelUserInfo{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: jujuparams.ModelAdminAccess, }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: jujuparams.ModelAdminAccess, }}, AgentVersion: &jujuversion.Current, @@ -372,7 +372,7 @@ func (s *modelManagerSuite) TestModelInfo(c *gc.C) { CloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), CloudRegion: jimmtest.TestCloudRegionName, CloudCredentialTag: s.Model3.CloudCredential.Tag().String(), - OwnerTag: names.NewUserTag("charlie@external").String(), + OwnerTag: names.NewUserTag("charlie@canonical.com").String(), Life: life.Alive, Status: jujuparams.EntityStatus{ Status: status.Available, @@ -381,7 +381,7 @@ func (s *modelManagerSuite) TestModelInfo(c *gc.C) { Level: "unsupported", }, Users: []jujuparams.ModelUserInfo{{ - UserName: "bob@external", + UserName: "bob@canonical.com", Access: jujuparams.ModelReadAccess, }}, AgentVersion: &jujuversion.Current, @@ -403,7 +403,7 @@ func (s *modelManagerSuite) TestModelInfo(c *gc.C) { CloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), CloudRegion: jimmtest.TestCloudRegionName, CloudCredentialTag: s.Model2.CloudCredential.Tag().String(), - OwnerTag: names.NewUserTag("charlie@external").String(), + OwnerTag: names.NewUserTag("charlie@canonical.com").String(), Life: life.Alive, Status: jujuparams.EntityStatus{ Status: status.Available, @@ -412,7 +412,7 @@ func (s *modelManagerSuite) TestModelInfo(c *gc.C) { Level: "unsupported", }, Users: []jujuparams.ModelUserInfo{{ - UserName: "bob@external", + UserName: "bob@canonical.com", Access: jujuparams.ModelWriteAccess, }}, Machines: []jujuparams.ModelMachineInfo{{ @@ -455,7 +455,7 @@ func (s *modelManagerSuite) TestModelInfo(c *gc.C) { CloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), CloudRegion: jimmtest.TestCloudRegionName, CloudCredentialTag: s.Model2.CloudCredential.Tag().String(), - OwnerTag: names.NewUserTag("charlie@external").String(), + OwnerTag: names.NewUserTag("charlie@canonical.com").String(), Life: life.Alive, Status: jujuparams.EntityStatus{ Status: status.Available, @@ -464,13 +464,13 @@ func (s *modelManagerSuite) TestModelInfo(c *gc.C) { Level: "unsupported", }, Users: []jujuparams.ModelUserInfo{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: jujuparams.ModelAdminAccess, }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: jujuparams.ModelAdminAccess, }, { - UserName: "charlie@external", + UserName: "charlie@canonical.com", Access: jujuparams.ModelAdminAccess, }}, AgentVersion: &jujuversion.Current, @@ -491,9 +491,9 @@ func (s *modelManagerSuite) TestModelInfo(c *gc.C) { } func (s *modelManagerSuite) TestModelInfoDisableControllerUUIDMasking(c *gc.C) { - mt4 := s.AddModel(c, names.NewUserTag("charlie@external"), "model-4", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, s.Model2.CloudCredential.ResourceTag()) + mt4 := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-4", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, s.Model2.CloudCredential.ResourceTag()) - mt5 := s.AddModel(c, names.NewUserTag("charlie@external"), "model-5", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, s.Model2.CloudCredential.ResourceTag()) + mt5 := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-5", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, s.Model2.CloudCredential.ResourceTag()) // Add some machines to one of the models state, err := s.StatePool.Get(s.Model3.Tag().Id()) @@ -518,10 +518,10 @@ func (s *modelManagerSuite) TestModelInfoDisableControllerUUIDMasking(c *gc.C) { }) f.MakeMachine(c, nil) - s.Candid.AddUser("bob", "controller-admin") + s.AddAdminUser(c, "bob@canonical.com") // we make bob a jimm administrator - bob := openfga.NewUser(&dbmodel.Identity{Name: "bob@external"}, s.OFGAClient) + bob := openfga.NewUser(&dbmodel.Identity{Name: "bob@canonical.com"}, s.OFGAClient) err = bob.SetControllerAccess(context.Background(), s.JIMM.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, gc.Equals, nil) @@ -572,10 +572,10 @@ func (s *modelManagerSuite) TestModelInfoDisableControllerUUIDMasking(c *gc.C) { Level: "unsupported", }, Users: []jujuparams.ModelUserInfo{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: jujuparams.ModelAdminAccess, }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: jujuparams.ModelAdminAccess, }}, AgentVersion: &jujuversion.Current, @@ -606,13 +606,13 @@ func (s *modelManagerSuite) TestModelInfoDisableControllerUUIDMasking(c *gc.C) { Level: "unsupported", }, Users: []jujuparams.ModelUserInfo{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: jujuparams.ModelAdminAccess, }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: jujuparams.ModelAdminAccess, }, { - UserName: "charlie@external", + UserName: "charlie@canonical.com", Access: jujuparams.ModelAdminAccess, }}, AgentVersion: &jujuversion.Current, @@ -643,13 +643,13 @@ func (s *modelManagerSuite) TestModelInfoDisableControllerUUIDMasking(c *gc.C) { Level: "unsupported", }, Users: []jujuparams.ModelUserInfo{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: jujuparams.ModelAdminAccess, }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: jujuparams.ModelAdminAccess, }, { - UserName: "charlie@external", + UserName: "charlie@canonical.com", Access: jujuparams.ModelAdminAccess, }}, Machines: []jujuparams.ModelMachineInfo{{ @@ -692,7 +692,7 @@ func (s *modelManagerSuite) TestModelInfoDisableControllerUUIDMasking(c *gc.C) { CloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), CloudRegion: jimmtest.TestCloudRegionName, CloudCredentialTag: s.Model2.CloudCredential.Tag().String(), - OwnerTag: names.NewUserTag("charlie@external").String(), + OwnerTag: names.NewUserTag("charlie@canonical.com").String(), Life: life.Alive, Status: jujuparams.EntityStatus{ Status: status.Available, @@ -701,13 +701,13 @@ func (s *modelManagerSuite) TestModelInfoDisableControllerUUIDMasking(c *gc.C) { Level: "unsupported", }, Users: []jujuparams.ModelUserInfo{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: jujuparams.ModelAdminAccess, }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: jujuparams.ModelAdminAccess, }, { - UserName: "charlie@external", + UserName: "charlie@canonical.com", Access: jujuparams.ModelAdminAccess, }}, Machines: []jujuparams.ModelMachineInfo{{ @@ -750,7 +750,7 @@ func (s *modelManagerSuite) TestModelInfoDisableControllerUUIDMasking(c *gc.C) { CloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), CloudRegion: jimmtest.TestCloudRegionName, CloudCredentialTag: s.Model2.CloudCredential.Tag().String(), - OwnerTag: names.NewUserTag("charlie@external").String(), + OwnerTag: names.NewUserTag("charlie@canonical.com").String(), Life: life.Alive, Status: jujuparams.EntityStatus{ Status: status.Available, @@ -759,13 +759,13 @@ func (s *modelManagerSuite) TestModelInfoDisableControllerUUIDMasking(c *gc.C) { Level: "unsupported", }, Users: []jujuparams.ModelUserInfo{{ - UserName: "alice@external", + UserName: "alice@canonical.com", Access: jujuparams.ModelAdminAccess, }, { - UserName: "bob@external", + UserName: "bob@canonical.com", Access: jujuparams.ModelAdminAccess, }, { - UserName: "charlie@external", + UserName: "charlie@canonical.com", Access: jujuparams.ModelAdminAccess, }}, AgentVersion: &jujuversion.Current, @@ -797,27 +797,27 @@ var createModelTests = []struct { }{{ about: "success", name: "model", - ownerTag: names.NewUserTag("bob@external").String(), + ownerTag: names.NewUserTag("bob@canonical.com").String(), cloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), - credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@external_cred", + credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@canonical.com_cred", }, { about: "unauthorized user", name: "model-2", - ownerTag: names.NewUserTag("charlie@external").String(), + ownerTag: names.NewUserTag("charlie@canonical.com").String(), cloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), - credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@external_cred", + credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@canonical.com_cred", expectError: `unauthorized \(unauthorized access\)`, }, { about: "existing model name", name: "model-1", - ownerTag: names.NewUserTag("bob@external").String(), + ownerTag: names.NewUserTag("bob@canonical.com").String(), cloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), - credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@external_cred", - expectError: "model bob@external/model-1 already exists \\(already exists\\)", + credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@canonical.com_cred", + expectError: "model bob@canonical.com/model-1 already exists \\(already exists\\)", }, { about: "no controller", name: "model-3", - ownerTag: names.NewUserTag("bob@external").String(), + ownerTag: names.NewUserTag("bob@canonical.com").String(), region: "no-such-region", cloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), credentialTag: "", @@ -827,46 +827,46 @@ var createModelTests = []struct { name: "model-4", ownerTag: names.NewUserTag("bob").String(), cloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), - credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@external_cred", + credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@canonical.com_cred", expectError: `unauthorized \(unauthorized access\)`, }, { about: "invalid user", name: "model-5", - ownerTag: "user-bob/test@external", + ownerTag: "user-bob/test@canonical.com", cloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), - credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@external_cred", - expectError: `"user-bob/test@external" is not a valid user tag \(bad request\)`, + credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@canonical.com_cred", + expectError: `"user-bob/test@canonical.com" is not a valid user tag \(bad request\)`, }, { about: "specific cloud", name: "model-6", - ownerTag: names.NewUserTag("bob@external").String(), + ownerTag: names.NewUserTag("bob@canonical.com").String(), cloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), - credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@external_cred", + credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@canonical.com_cred", }, { about: "specific cloud and region", name: "model-7", - ownerTag: names.NewUserTag("bob@external").String(), + ownerTag: names.NewUserTag("bob@canonical.com").String(), cloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), region: jimmtest.TestCloudRegionName, - credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@external_cred", + credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@canonical.com_cred", }, { about: "bad cloud tag", name: "model-8", - ownerTag: names.NewUserTag("bob@external").String(), + ownerTag: names.NewUserTag("bob@canonical.com").String(), cloudTag: "not-a-cloud-tag", - credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@external_cred1", + credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@canonical.com_cred1", expectError: `"not-a-cloud-tag" is not a valid tag \(bad request\)`, }, { about: "no cloud tag", name: "model-8", - ownerTag: names.NewUserTag("bob@external").String(), + ownerTag: names.NewUserTag("bob@canonical.com").String(), cloudTag: "", - credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@external_cred1", + credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@canonical.com_cred1", expectError: `no cloud specified for model; please specify one`, }, { about: "no credential tag selects unambigous creds", name: "model-8", - ownerTag: names.NewUserTag("bob@external").String(), + ownerTag: names.NewUserTag("bob@canonical.com").String(), cloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), region: jimmtest.TestCloudRegionName, }} @@ -927,7 +927,7 @@ func (s *modelManagerSuite) TestGrantAndRevokeModel(c *gc.C) { c.Assert(res, gc.HasLen, 1) c.Assert(res[0].Error, gc.ErrorMatches, "unauthorized") - err = client.GrantModel("charlie@external", "write", s.Model.UUID.String) + err = client.GrantModel("charlie@canonical.com", "write", s.Model.UUID.String) c.Assert(err, gc.Equals, nil) res, err = client2.ModelInfo([]names.ModelTag{s.Model.ResourceTag()}) @@ -936,7 +936,7 @@ func (s *modelManagerSuite) TestGrantAndRevokeModel(c *gc.C) { c.Assert(res[0].Error, gc.IsNil) c.Assert(res[0].Result.UUID, gc.Equals, s.Model.UUID.String) - err = client.RevokeModel("charlie@external", "read", s.Model.UUID.String) + err = client.RevokeModel("charlie@canonical.com", "read", s.Model.UUID.String) c.Assert(err, gc.Equals, nil) res, err = client2.ModelInfo([]names.ModelTag{s.Model.ResourceTag()}) @@ -956,7 +956,7 @@ func (s *modelManagerSuite) TestUserRevokeOwnAccess(c *gc.C) { defer conn2.Close() client2 := modelmanager.NewClient(conn2) - err := client.GrantModel("charlie@external", "read", s.Model.UUID.String) + err := client.GrantModel("charlie@canonical.com", "read", s.Model.UUID.String) c.Assert(err, gc.Equals, nil) res, err := client2.ModelInfo([]names.ModelTag{names.NewModelTag(s.Model.UUID.String)}) @@ -965,7 +965,7 @@ func (s *modelManagerSuite) TestUserRevokeOwnAccess(c *gc.C) { c.Assert(res[0].Error, gc.IsNil) c.Assert(res[0].Result.UUID, gc.Equals, s.Model.UUID.String) - err = client2.RevokeModel("charlie@external", "read", s.Model.UUID.String) + err = client2.RevokeModel("charlie@canonical.com", "read", s.Model.UUID.String) c.Assert(err, gc.Equals, nil) res, err = client2.ModelInfo([]names.ModelTag{names.NewModelTag(s.Model.UUID.String)}) @@ -986,7 +986,7 @@ func (s *modelManagerSuite) TestModifyModelAccessErrors(c *gc.C) { }{{ about: "unauthorized", modifyModelAccess: jujuparams.ModifyModelAccess{ - UserTag: names.NewUserTag("eve@external").String(), + UserTag: names.NewUserTag("eve@canonical.com").String(), Action: jujuparams.GrantModelAccess, Access: jujuparams.ModelReadAccess, ModelTag: s.Model2.Tag().String(), @@ -1004,7 +1004,7 @@ func (s *modelManagerSuite) TestModifyModelAccessErrors(c *gc.C) { }, { about: "no such model", modifyModelAccess: jujuparams.ModifyModelAccess{ - UserTag: names.NewUserTag("eve@external").String(), + UserTag: names.NewUserTag("eve@canonical.com").String(), Action: jujuparams.GrantModelAccess, Access: jujuparams.ModelReadAccess, ModelTag: names.NewModelTag("00000000-0000-0000-0000-000000000000").String(), @@ -1013,7 +1013,7 @@ func (s *modelManagerSuite) TestModifyModelAccessErrors(c *gc.C) { }, { about: "invalid model tag", modifyModelAccess: jujuparams.ModifyModelAccess{ - UserTag: names.NewUserTag("eve@external").String(), + UserTag: names.NewUserTag("eve@canonical.com").String(), Action: jujuparams.GrantModelAccess, Access: jujuparams.ModelReadAccess, ModelTag: "not-a-model-tag", @@ -1031,7 +1031,7 @@ func (s *modelManagerSuite) TestModifyModelAccessErrors(c *gc.C) { }, { about: "unknown action", modifyModelAccess: jujuparams.ModifyModelAccess{ - UserTag: names.NewUserTag("eve@external").String(), + UserTag: names.NewUserTag("eve@canonical.com").String(), Action: "not-an-action", Access: jujuparams.ModelReadAccess, ModelTag: s.Model.Tag().String(), @@ -1040,7 +1040,7 @@ func (s *modelManagerSuite) TestModifyModelAccessErrors(c *gc.C) { }, { about: "invalid access", modifyModelAccess: jujuparams.ModifyModelAccess{ - UserTag: names.NewUserTag("eve@external").String(), + UserTag: names.NewUserTag("eve@canonical.com").String(), Action: jujuparams.GrantModelAccess, Access: "not-an-access", ModelTag: s.Model.Tag().String(), @@ -1141,7 +1141,7 @@ func (s *modelManagerSuite) TestChangeModelCredential(c *gc.C) { defer conn.Close() modelTag := s.Model.ResourceTag() - credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@external/cred2") + credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@canonical.com/cred2") s.UpdateCloudCredential(c, credTag, jujuparams.CloudCredential{AuthType: "empty"}) client := modelmanager.NewClient(conn) @@ -1159,7 +1159,7 @@ func (s *modelManagerSuite) TestChangeModelCredentialUnauthorizedModel(c *gc.C) defer conn.Close() modelTag := s.Model.ResourceTag() - credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@external/cred2") + credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@canonical.com/cred2") client := modelmanager.NewClient(conn) err := client.ChangeModelCredential(modelTag, credTag) c.Assert(err, gc.ErrorMatches, `unauthorized`) @@ -1170,7 +1170,7 @@ func (s *modelManagerSuite) TestChangeModelCredentialUnauthorizedCredential(c *g defer conn.Close() modelTag := s.Model.ResourceTag() - credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/alice@external/cred2") + credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/alice@canonical.com/cred2") client := modelmanager.NewClient(conn) err := client.ChangeModelCredential(modelTag, credTag) c.Assert(err, gc.ErrorMatches, `unauthorized`) @@ -1192,10 +1192,10 @@ func (s *modelManagerSuite) TestChangeModelCredentialNotFoundCredential(c *gc.C) defer conn.Close() modelTag := s.Model.ResourceTag() - credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@external/cred2") + credTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@canonical.com/cred2") client := modelmanager.NewClient(conn) err := client.ChangeModelCredential(modelTag, credTag) - c.Assert(err, gc.ErrorMatches, `cloudcredential "`+jimmtest.TestCloudName+`/bob@external/cred2" not found`) + c.Assert(err, gc.ErrorMatches, `cloudcredential "`+jimmtest.TestCloudName+`/bob@canonical.com/cred2" not found`) } func (s *modelManagerSuite) TestChangeModelCredentialLocalUserCredential(c *gc.C) { @@ -1211,7 +1211,7 @@ func (s *modelManagerSuite) TestChangeModelCredentialLocalUserCredential(c *gc.C func (s *modelManagerSuite) TestValidateModelUpgrades(c *gc.C) { c.Skip("3.2 no longer implements ValidateModelUpgrade") - conn := s.open(c, nil, "alice@external") + conn := s.open(c, nil, "alice@canonical.com") defer conn.Close() modelTag := s.Model.ResourceTag() @@ -1339,7 +1339,7 @@ func (s *caasModelManagerSuite) SetUpTest(c *gc.C) { }, false) c.Assert(err, gc.Equals, nil) - s.cred = names.NewCloudCredentialTag("bob-cloud/bob@external/k8s") + s.cred = names.NewCloudCredentialTag("bob-cloud/bob@canonical.com/k8s") cred := cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ "username": kubetest.Username, "password": kubetest.Password, @@ -1361,7 +1361,7 @@ func (s *caasModelManagerSuite) TestCreateModelKubernetes(c *gc.C) { defer conn.Close() client := modelmanager.NewClient(conn) - mi, err := client.CreateModel("k8s-model-1", "bob@external", "bob-cloud", "", s.cred, nil) + mi, err := client.CreateModel("k8s-model-1", "bob@canonical.com", "bob-cloud", "", s.cred, nil) c.Assert(err, gc.Equals, nil) c.Assert(mi.Name, gc.Equals, "k8s-model-1") @@ -1369,7 +1369,7 @@ func (s *caasModelManagerSuite) TestCreateModelKubernetes(c *gc.C) { c.Assert(mi.ProviderType, gc.Equals, "kubernetes") c.Assert(mi.Cloud, gc.Equals, "bob-cloud") c.Assert(mi.CloudRegion, gc.Equals, "default") - c.Assert(mi.Owner, gc.Equals, "bob@external") + c.Assert(mi.Owner, gc.Equals, "bob@canonical.com") } func (s *caasModelManagerSuite) TestListCAASModelSummaries(c *gc.C) { @@ -1380,7 +1380,7 @@ func (s *caasModelManagerSuite) TestListCAASModelSummaries(c *gc.C) { defer conn.Close() client := modelmanager.NewClient(conn) - mi, err := client.CreateModel("k8s-model-1", "bob@external", "bob-cloud", "", s.cred, nil) + mi, err := client.CreateModel("k8s-model-1", "bob@canonical.com", "bob-cloud", "", s.cred, nil) c.Assert(err, gc.Equals, nil) models, err := client.ListModelSummaries("bob", false) @@ -1399,7 +1399,7 @@ func (s *caasModelManagerSuite) TestListCAASModelSummaries(c *gc.C) { Cloud: "bob-cloud", CloudRegion: "default", CloudCredential: s.cred.Id(), - Owner: "bob@external", + Owner: "bob@canonical.com", Life: life.Value(constants.ALIVE.String()), Status: base.Status{ Status: status.Available, @@ -1430,8 +1430,8 @@ func (s *caasModelManagerSuite) TestListCAASModelSummaries(c *gc.C) { DefaultSeries: "jammy", Cloud: jimmtest.TestCloudName, CloudRegion: jimmtest.TestCloudRegionName, - CloudCredential: jimmtest.TestCloudName + "/bob@external/cred", - Owner: "bob@external", + CloudCredential: jimmtest.TestCloudName + "/bob@canonical.com/cred", + Owner: "bob@canonical.com", Life: life.Value(constants.ALIVE.String()), Status: base.Status{ Status: status.Available, @@ -1452,8 +1452,8 @@ func (s *caasModelManagerSuite) TestListCAASModelSummaries(c *gc.C) { DefaultSeries: "jammy", Cloud: jimmtest.TestCloudName, CloudRegion: jimmtest.TestCloudRegionName, - CloudCredential: jimmtest.TestCloudName + "/charlie@external/cred", - Owner: "charlie@external", + CloudCredential: jimmtest.TestCloudName + "/charlie@canonical.com/cred", + Owner: "charlie@canonical.com", Life: life.Value(constants.ALIVE.String()), Status: base.Status{ Status: status.Available, @@ -1476,7 +1476,7 @@ func (s *caasModelManagerSuite) TestListCAASModels(c *gc.C) { defer conn.Close() client := modelmanager.NewClient(conn) - mi, err := client.CreateModel("k8s-model-1", "bob@external", "bob-cloud", "", s.cred, nil) + mi, err := client.CreateModel("k8s-model-1", "bob@canonical.com", "bob-cloud", "", s.cred, nil) c.Assert(err, gc.Equals, nil) models, err := client.ListModels("bob") @@ -1484,17 +1484,17 @@ func (s *caasModelManagerSuite) TestListCAASModels(c *gc.C) { c.Assert(models, jc.SameContents, []base.UserModel{{ Name: "k8s-model-1", UUID: mi.UUID, - Owner: "bob@external", + Owner: "bob@canonical.com", Type: "caas", }, { Name: "model-1", UUID: s.Model.UUID.String, - Owner: "bob@external", + Owner: "bob@canonical.com", Type: "iaas", }, { Name: "model-3", UUID: s.Model3.UUID.String, - Owner: "charlie@external", + Owner: "charlie@canonical.com", Type: "iaas", }}) } diff --git a/internal/jujuapi/service_account.go b/internal/jujuapi/service_account.go index 9527a1dbb..9b9806f01 100644 --- a/internal/jujuapi/service_account.go +++ b/internal/jujuapi/service_account.go @@ -6,7 +6,7 @@ import ( "context" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" apiparams "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/dbmodel" diff --git a/internal/jujuapi/service_account_test.go b/internal/jujuapi/service_account_test.go index 34c05b004..07e044875 100644 --- a/internal/jujuapi/service_account_test.go +++ b/internal/jujuapi/service_account_test.go @@ -8,7 +8,7 @@ import ( qt "github.com/frankban/quicktest" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/api/params" @@ -504,7 +504,7 @@ func (s *serviceAccountSuite) TestUpdateServiceAccountCredentialsIntegration(c * serviceAccount := jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be") tuple := openfga.Tuple{ - Object: ofganames.ConvertTag(names.NewUserTag("bob@external")), + Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(serviceAccount), } diff --git a/internal/jujuapi/usermanager_test.go b/internal/jujuapi/usermanager_test.go index cdee1b3db..9d3aa0f21 100644 --- a/internal/jujuapi/usermanager_test.go +++ b/internal/jujuapi/usermanager_test.go @@ -68,14 +68,14 @@ func (s *userManagerSuite) TestUserInfoSpecifiedUser(c *gc.C) { defer conn.Close() client := usermanager.NewClient(conn) - users, err := client.UserInfo([]string{"alice@external"}, usermanager.AllUsers) + users, err := client.UserInfo([]string{"alice@canonical.com"}, usermanager.AllUsers) c.Assert(err, gc.Equals, nil) c.Assert(len(users), gc.Equals, 1) c.Assert(users[0].DateCreated.IsZero(), gc.Equals, false) users[0].DateCreated = time.Time{} users[0].LastConnection = nil c.Assert(users[0], jc.DeepEquals, jujuparams.UserInfo{ - Username: "alice@external", + Username: "alice@canonical.com", DisplayName: "alice", Access: "", }) @@ -86,8 +86,8 @@ func (s *userManagerSuite) TestUserInfoSpecifiedUsers(c *gc.C) { defer conn.Close() client := usermanager.NewClient(conn) - users, err := client.UserInfo([]string{"alice@external", "bob@external"}, usermanager.AllUsers) - c.Assert(err, gc.ErrorMatches, "bob@external: unauthorized access") + users, err := client.UserInfo([]string{"alice@canonical.com", "bob@canonical.com"}, usermanager.AllUsers) + c.Assert(err, gc.ErrorMatches, "bob@canonical.com: unauthorized access") c.Assert(users, gc.HasLen, 0) } @@ -102,9 +102,10 @@ func (s *userManagerSuite) TestUserInfoWithDomain(c *gc.C) { c.Assert(users[0].DateCreated.IsZero(), gc.Equals, false) users[0].DateCreated = time.Time{} c.Assert(users[0], jc.DeepEquals, jujuparams.UserInfo{ - Username: "alice@mydomain", - DisplayName: "alice", - Access: "", + Username: "alice@mydomain", + DisplayName: "", + Access: "", + LastConnection: users[0].LastConnection, }) } @@ -113,8 +114,8 @@ func (s *userManagerSuite) TestUserInfoInvalidUsername(c *gc.C) { defer conn.Close() client := usermanager.NewClient(conn) - users, err := client.UserInfo([]string{"alice-@external"}, usermanager.AllUsers) - c.Assert(err, gc.ErrorMatches, `"alice-@external" is not a valid username`) + users, err := client.UserInfo([]string{"alice-@canonical.com"}, usermanager.AllUsers) + c.Assert(err, gc.ErrorMatches, `"alice-@canonical.com" is not a valid username`) c.Assert(users, gc.HasLen, 0) } diff --git a/internal/jujuapi/websocket.go b/internal/jujuapi/websocket.go index 7aa95d3b7..0d92260a5 100644 --- a/internal/jujuapi/websocket.go +++ b/internal/jujuapi/websocket.go @@ -130,7 +130,8 @@ func modelInfoFromPath(path string) (uuid string, finalPath string, err error) { // ServeWS implements jimmhttp.WSServer. func (s modelProxyServer) ServeWS(ctx context.Context, clientConn *websocket.Conn) { - jwtGenerator := jimm.NewJWTGenerator(s.jimm.Authenticator, &s.jimm.Database, s.jimm, s.jimm.JWTService) + // TODO(CSS-7331) Refactor model proxy for new login methods + jwtGenerator := jimm.NewJWTGenerator(nil, &s.jimm.Database, s.jimm, s.jimm.JWTService) connectionFunc := controllerConnectionFunc(s, &jwtGenerator) zapctx.Debug(ctx, "Starting proxier") auditLogger := s.jimm.AddAuditLogEntry diff --git a/internal/jujuapi/websocket_test.go b/internal/jujuapi/websocket_test.go index 0b7aa1d23..087accfc1 100644 --- a/internal/jujuapi/websocket_test.go +++ b/internal/jujuapi/websocket_test.go @@ -11,12 +11,9 @@ import ( "net/http/httptest" "net/url" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" "github.com/juju/juju/api" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/internal/dbmodel" @@ -28,7 +25,6 @@ import ( ) type websocketSuite struct { - jimmtest.CandidSuite jimmtest.BootstrapSuite Params jujuapi.Params @@ -46,15 +42,9 @@ func (s *websocketSuite) SetUpTest(c *gc.C) { ctx, cancelFnc := context.WithCancel(context.Background()) s.cancelFnc = cancelFnc - s.ControllerAdmins = []string{"controller-admin"} - - s.CandidSuite.SetUpTest(c) s.BootstrapSuite.SetUpTest(c) - s.JIMM.Authenticator = s.Authenticator - s.Params.ControllerUUID = "914487b5-60e7-42bb-bd63-1adc3fd3a388" - s.Params.IdentityLocation = s.Candid.URL.String() mux := http.NewServeMux() mux.Handle("/api", jujuapi.APIHandler(ctx, s.JIMM, s.Params)) @@ -65,22 +55,22 @@ func (s *websocketSuite) SetUpTest(c *gc.C) { s.APIHandler = mux s.HTTP = httptest.NewTLSServer(s.APIHandler) - s.Candid.AddUser("alice") + s.AddAdminUser(c, "alice@canonical.com") - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@external/cred") + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/charlie@canonical.com/cred") s.UpdateCloudCredential(c, cct, jujuparams.CloudCredential{AuthType: "empty"}) s.Credential2 = new(dbmodel.CloudCredential) s.Credential2.SetTag(cct) err := s.JIMM.Database.GetCloudCredential(ctx, s.Credential2) c.Assert(err, gc.Equals, nil) - mt := s.AddModel(c, names.NewUserTag("charlie@external"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) + mt := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) s.Model2 = new(dbmodel.Model) s.Model2.SetTag(mt) err = s.JIMM.Database.GetModel(ctx, s.Model2) c.Assert(err, gc.Equals, nil) - mt = s.AddModel(c, names.NewUserTag("charlie@external"), "model-3", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) + mt = s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-3", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) s.Model3 = new(dbmodel.Model) s.Model3.SetTag(mt) err = s.JIMM.Database.GetModel(ctx, s.Model3) @@ -88,7 +78,7 @@ func (s *websocketSuite) SetUpTest(c *gc.C) { bob := openfga.NewUser( &dbmodel.Identity{ - Name: "bob@external", + Name: "bob@canonical.com", }, s.OFGAClient, ) @@ -104,7 +94,6 @@ func (s *websocketSuite) TearDownTest(c *gc.C) { s.HTTP.Close() } s.BootstrapSuite.TearDownTest(c) - s.CandidSuite.TearDownTest(c) } // openNoAssert creates a new websocket connection to the test server, using the @@ -128,24 +117,11 @@ func (s *websocketSuite) openNoAssert(c *gc.C, info *api.Info, username string) c.Assert(err, gc.Equals, nil) inf.CACert = w.String() - s.Candid.AddUser(username) - key := s.Candid.UserPublicKey(username) - bClient := httpbakery.NewClient() - bClient.Key = &bakery.KeyPair{ - Public: bakery.PublicKey{Key: bakery.Key(key.Public.Key)}, - Private: bakery.PrivateKey{Key: bakery.Key(key.Private.Key)}, - } - agent.SetUpAuth(bClient, &agent.AuthInfo{ - Key: bClient.Key, - Agents: []agent.Agent{{ - URL: s.Candid.URL.String(), - Username: username, - }}, - }) + lp := jimmtest.NewUserSessionLogin(username) return api.Open(&inf, api.DialOpts{ InsecureSkipVerify: true, - BakeryClient: bClient, + LoginProvider: lp, }) } @@ -172,41 +148,24 @@ func (s *proxySuite) TestConnectToModel(c *gc.C) { c.Assert(err, gc.ErrorMatches, `no such request - method Admin.TestMethod is not implemented \(not implemented\)`) } -func (s *proxySuite) TestConnectToModelAndLogin(c *gc.C) { - ctx := context.Background() - alice := names.NewUserTag("alice") - aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.JIMM.OpenFGAClient) - err := aliceUser.SetControllerAccess(ctx, s.Model.Controller.ResourceTag(), ofganames.AdministratorRelation) - c.Assert(err, gc.IsNil) - conn, err := s.openNoAssert(c, &api.Info{ - ModelTag: s.Model.ResourceTag(), - SkipLogin: false, - }, "alice") - if err == nil { - defer conn.Close() - } - c.Assert(err, gc.Equals, nil) -} - -// TestConnectToModelNoBakeryClient ensures that authentication is in fact -// happening, without a bakery client the test should see an error from Candid. -func (s *proxySuite) TestConnectToModelNoBakeryClient(c *gc.C) { - inf := api.Info{ - ModelTag: s.Model.ResourceTag(), - SkipLogin: false, - } - u, err := url.Parse(s.HTTP.URL) - c.Assert(err, gc.Equals, nil) - inf.Addrs = []string{ - u.Host, - } - c.Assert(err, gc.Equals, nil) - _, err = api.Open(&inf, api.DialOpts{ - InsecureSkipVerify: true, - BakeryClient: nil, - }) - c.Assert(err, gc.ErrorMatches, "interaction required but not possible") -} +// TODO(CSS-7331) Refactor model proxy for new login methods +// func (s *proxySuite) TestConnectToModelAndLogin(c *gc.C) { +// ctx := context.Background() +// alice := names.NewUserTag("alice@canonical.com") +// aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.JIMM.OpenFGAClient) +// err := aliceUser.SetControllerAccess(ctx, s.Model.Controller.ResourceTag(), ofganames.AdministratorRelation) +// c.Assert(err, gc.IsNil) +// conn, err := s.openNoAssert(c, &api.Info{ +// ModelTag: s.Model.ResourceTag(), +// SkipLogin: false, +// }, "alice") +// if err == nil { +// defer conn.Close() +// } +// c.Assert(err, gc.Equals, nil) +// } + +// TODO(CSS-7331) Add more tests for model proxy and new login methods. type pathTestSuite struct{} diff --git a/internal/jujuclient/applicationoffers.go b/internal/jujuclient/applicationoffers.go index c2a096e32..fd1117cc5 100644 --- a/internal/jujuclient/applicationoffers.go +++ b/internal/jujuclient/applicationoffers.go @@ -9,7 +9,7 @@ import ( jujuerrors "github.com/juju/errors" "github.com/juju/juju/core/crossmodel" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/errors" ) diff --git a/internal/jujuclient/applicationoffers_test.go b/internal/jujuclient/applicationoffers_test.go index d35a81e03..0f78ec0b9 100644 --- a/internal/jujuclient/applicationoffers_test.go +++ b/internal/jujuclient/applicationoffers_test.go @@ -11,12 +11,11 @@ import ( "github.com/juju/juju/core/crossmodel" jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/juju/testing/factory" - "github.com/juju/names/v4" + "github.com/juju/names/v5" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" "github.com/canonical/jimm/internal/jimmtest" - ofganames "github.com/canonical/jimm/internal/openfga/names" ) type applicationoffersSuite struct { @@ -33,7 +32,7 @@ func (s *applicationoffersSuite) SetUpTest(c *gc.C) { ctx := context.Background() err := s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("test-user@external").String(), + OwnerTag: names.NewUserTag("test-user@canonical.com").String(), }, &s.modelInfo) c.Assert(err, gc.Equals, nil) } @@ -56,7 +55,7 @@ func (s *applicationoffersSuite) TestOffer(c *gc.C) { c.Assert(err, gc.Equals, nil) offerURL := crossmodel.OfferURL{ - User: "test-user@external", + User: "test-user@canonical.com", ModelName: s.modelInfo.Name, ApplicationName: "test-offer", } @@ -92,7 +91,7 @@ func (s *applicationoffersSuite) TestOffer(c *gc.C) { func (s *applicationoffersSuite) TestOfferError(c *gc.C) { offerURL := crossmodel.OfferURL{ - User: "test-user@external", + User: "test-user@canonical.com", ModelName: s.modelInfo.Name, ApplicationName: "test-offer", } @@ -147,7 +146,7 @@ func (s *applicationoffersSuite) TestListApplicationOffersMatching(c *gc.C) { ctx := context.Background() offerURL := crossmodel.OfferURL{ - User: "test-user@external", + User: "test-user@canonical.com", ModelName: s.modelInfo.Name, ApplicationName: "test-offer", } @@ -199,7 +198,7 @@ func (s *applicationoffersSuite) TestListApplicationOffersNoMatch(c *gc.C) { ctx := context.Background() offerURL := crossmodel.OfferURL{ - User: "test-user@external", + User: "test-user@canonical.com", ModelName: s.modelInfo.Name, ApplicationName: "test-offer", } @@ -263,7 +262,7 @@ func (s *applicationoffersSuite) TestFindApplicationOffersMatching(c *gc.C) { ctx := context.Background() offerURL := crossmodel.OfferURL{ - User: "test-user@external", + User: "test-user@canonical.com", ModelName: s.modelInfo.Name, ApplicationName: "test-offer", } @@ -315,7 +314,7 @@ func (s *applicationoffersSuite) TestFindApplicationOffersNoMatch(c *gc.C) { ctx := context.Background() offerURL := crossmodel.OfferURL{ - User: "test-user@external", + User: "test-user@canonical.com", ModelName: s.modelInfo.Name, ApplicationName: "test-offer", } @@ -363,7 +362,7 @@ func (s *applicationoffersSuite) TestGetApplicationOffer(c *gc.C) { ctx := context.Background() offerURL := crossmodel.OfferURL{ - User: "test-user@external", + User: "test-user@canonical.com", ModelName: s.modelInfo.Name, ApplicationName: "test-offer", } @@ -410,7 +409,7 @@ func (s *applicationoffersSuite) TestGetApplicationOffer(c *gc.C) { DisplayName: "admin", Access: string(jujuparams.OfferAdminAccess), }, { - UserName: ofganames.EveryoneUser, + UserName: "everyone@external", Access: string(jujuparams.OfferReadAccess), }}, }, @@ -422,9 +421,9 @@ func (s *applicationoffersSuite) TestGetApplicationOfferNotFound(c *gc.C) { ctx := context.Background() var info jujuparams.ApplicationOfferAdminDetails - info.OfferURL = "test-user@external/test-model.test-offer" + info.OfferURL = "test-user@canonical.com/test-model.test-offer" err := s.API.GetApplicationOffer(ctx, &info) - c.Assert(err, gc.ErrorMatches, `application offer "test-user@external/test-model.test-offer" not found`) + c.Assert(err, gc.ErrorMatches, `application offer "test-user@canonical.com/test-model.test-offer" not found`) } func (s *applicationoffersSuite) TestGrantApplicationOfferAccess(c *gc.C) { @@ -446,7 +445,7 @@ func (s *applicationoffersSuite) TestGrantApplicationOfferAccess(c *gc.C) { ctx := context.Background() offerURL := crossmodel.OfferURL{ - User: "test-user@external", + User: "test-user@canonical.com", ModelName: s.modelInfo.Name, ApplicationName: "test-offer", } @@ -464,7 +463,7 @@ func (s *applicationoffersSuite) TestGrantApplicationOfferAccess(c *gc.C) { ) c.Assert(err, gc.Equals, nil) - err = s.API.GrantApplicationOfferAccess(ctx, offerURL.String(), names.NewUserTag("test-user-2@external"), jujuparams.OfferConsumeAccess) + err = s.API.GrantApplicationOfferAccess(ctx, offerURL.String(), names.NewUserTag("test-user-2@canonical.com"), jujuparams.OfferConsumeAccess) c.Assert(err, gc.Equals, nil) var info jujuparams.ApplicationOfferAdminDetails @@ -495,10 +494,10 @@ func (s *applicationoffersSuite) TestGrantApplicationOfferAccess(c *gc.C) { DisplayName: "admin", Access: string(jujuparams.OfferAdminAccess), }, { - UserName: ofganames.EveryoneUser, + UserName: "everyone@external", Access: string(jujuparams.OfferReadAccess), }, { - UserName: "test-user-2@external", + UserName: "test-user-2@canonical.com", Access: string(jujuparams.OfferConsumeAccess), }}, }, @@ -508,9 +507,9 @@ func (s *applicationoffersSuite) TestGrantApplicationOfferAccess(c *gc.C) { func (s *applicationoffersSuite) TestGrantApplicationOfferAccessNotFound(c *gc.C) { ctx := context.Background() - offerURL := "test-user@external/test-model.test-offer" + offerURL := "test-user@canonical.com/test-model.test-offer" - err := s.API.GrantApplicationOfferAccess(ctx, offerURL, names.NewUserTag("test-user-2@external"), jujuparams.OfferConsumeAccess) + err := s.API.GrantApplicationOfferAccess(ctx, offerURL, names.NewUserTag("test-user-2@canonical.com"), jujuparams.OfferConsumeAccess) c.Check(err, gc.ErrorMatches, `offer "test-offer" not found`) } @@ -533,7 +532,7 @@ func (s *applicationoffersSuite) TestRevokeApplicationOfferAccess(c *gc.C) { ctx := context.Background() offerURL := crossmodel.OfferURL{ - User: "test-user@external", + User: "test-user@canonical.com", ModelName: s.modelInfo.Name, ApplicationName: "test-offer", } @@ -551,7 +550,7 @@ func (s *applicationoffersSuite) TestRevokeApplicationOfferAccess(c *gc.C) { ) c.Assert(err, gc.Equals, nil) - err = s.API.GrantApplicationOfferAccess(ctx, offerURL.String(), names.NewUserTag("test-user-2@external"), jujuparams.OfferConsumeAccess) + err = s.API.GrantApplicationOfferAccess(ctx, offerURL.String(), names.NewUserTag("test-user-2@canonical.com"), jujuparams.OfferConsumeAccess) c.Assert(err, gc.Equals, nil) var info jujuparams.ApplicationOfferAdminDetails @@ -569,7 +568,7 @@ func (s *applicationoffersSuite) TestRevokeApplicationOfferAccess(c *gc.C) { c.Check(info, jc.DeepEquals, jujuparams.ApplicationOfferAdminDetails{ ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ SourceModelTag: names.NewModelTag(s.modelInfo.UUID).String(), - OfferURL: "test-user@external/test-model.test-offer", + OfferURL: "test-user@canonical.com/test-model.test-offer", OfferName: "test-offer", ApplicationDescription: "A pretty popular blog engine", Endpoints: []jujuparams.RemoteEndpoint{{ @@ -583,17 +582,17 @@ func (s *applicationoffersSuite) TestRevokeApplicationOfferAccess(c *gc.C) { DisplayName: "admin", Access: string(jujuparams.OfferAdminAccess), }, { - UserName: ofganames.EveryoneUser, + UserName: "everyone@external", Access: string(jujuparams.OfferReadAccess), }, { - UserName: "test-user-2@external", + UserName: "test-user-2@canonical.com", Access: string(jujuparams.OfferConsumeAccess), }}, }, ApplicationName: "test-app", }) - err = s.API.RevokeApplicationOfferAccess(ctx, offerURL.String(), names.NewUserTag("test-user-2@external"), jujuparams.OfferConsumeAccess) + err = s.API.RevokeApplicationOfferAccess(ctx, offerURL.String(), names.NewUserTag("test-user-2@canonical.com"), jujuparams.OfferConsumeAccess) c.Assert(err, gc.Equals, nil) err = s.API.GetApplicationOffer(ctx, &info) @@ -622,10 +621,10 @@ func (s *applicationoffersSuite) TestRevokeApplicationOfferAccess(c *gc.C) { DisplayName: "admin", Access: string(jujuparams.OfferAdminAccess), }, { - UserName: ofganames.EveryoneUser, + UserName: "everyone@external", Access: string(jujuparams.OfferReadAccess), }, { - UserName: "test-user-2@external", + UserName: "test-user-2@canonical.com", Access: string(jujuparams.OfferReadAccess), }}, }, @@ -635,9 +634,9 @@ func (s *applicationoffersSuite) TestRevokeApplicationOfferAccess(c *gc.C) { func (s *applicationoffersSuite) TestRevokeApplicationOfferAccessNotFound(c *gc.C) { ctx := context.Background() - offerURL := "test-user@external/test-model.test-offer" + offerURL := "test-user@canonical.com/test-model.test-offer" - err := s.API.RevokeApplicationOfferAccess(ctx, offerURL, names.NewUserTag("test-user-2@external"), jujuparams.OfferConsumeAccess) + err := s.API.RevokeApplicationOfferAccess(ctx, offerURL, names.NewUserTag("test-user-2@canonical.com"), jujuparams.OfferConsumeAccess) c.Check(err, gc.ErrorMatches, `offer "test-offer" not found`) } @@ -660,7 +659,7 @@ func (s *applicationoffersSuite) TestDestroyApplicationOffer(c *gc.C) { ctx := context.Background() offerURL := crossmodel.OfferURL{ - User: "test-user@external", + User: "test-user@canonical.com", ModelName: s.modelInfo.Name, ApplicationName: "test-offer", } @@ -717,7 +716,7 @@ func (s *applicationoffersSuite) TestGetApplicationOfferConsumeDetails(c *gc.C) ctx := context.Background() offerURL := crossmodel.OfferURL{ - User: "test-user@external", + User: "test-user@canonical.com", ModelName: s.modelInfo.Name, ApplicationName: "test-offer", } @@ -751,7 +750,7 @@ func (s *applicationoffersSuite) TestGetApplicationOfferConsumeDetails(c *gc.C) c.Check(info, jimmtest.CmpEquals(cmpopts.SortSlices(lessF)), jujuparams.ConsumeOfferDetails{ Offer: &jujuparams.ApplicationOfferDetails{ SourceModelTag: names.NewModelTag(s.modelInfo.UUID).String(), - OfferURL: "test-user@external/test-model.test-offer", + OfferURL: "test-user@canonical.com/test-model.test-offer", OfferName: "test-offer", ApplicationDescription: "A pretty popular blog engine", Endpoints: []jujuparams.RemoteEndpoint{{ @@ -765,7 +764,7 @@ func (s *applicationoffersSuite) TestGetApplicationOfferConsumeDetails(c *gc.C) DisplayName: "admin", Access: "admin", }, { - UserName: ofganames.EveryoneUser, + UserName: "everyone@external", DisplayName: "", Access: "read", }}, @@ -781,8 +780,8 @@ func (s *applicationoffersSuite) TestGetApplicationOfferConsumeDetails(c *gc.C) func (s *applicationoffersSuite) TestGetApplicationOfferConsumeDetailsNotFound(c *gc.C) { var info jujuparams.ConsumeOfferDetails info.Offer = &jujuparams.ApplicationOfferDetails{ - OfferURL: "test-user@external/test-model.test-offer", + OfferURL: "test-user@canonical.com/test-model.test-offer", } - err := s.API.GetApplicationOfferConsumeDetails(context.Background(), names.NewUserTag("test-user@external"), &info, bakery.Version2) - c.Check(err, gc.ErrorMatches, `application offer "test-user@external/test-model.test-offer" not found`) + err := s.API.GetApplicationOfferConsumeDetails(context.Background(), names.NewUserTag("test-user@canonical.com"), &info, bakery.Version2) + c.Check(err, gc.ErrorMatches, `application offer "test-user@canonical.com/test-model.test-offer" not found`) } diff --git a/internal/jujuclient/client_test.go b/internal/jujuclient/client_test.go index 243bc0880..a2abd3846 100644 --- a/internal/jujuclient/client_test.go +++ b/internal/jujuclient/client_test.go @@ -8,7 +8,7 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" jujuparams "github.com/juju/juju/rpc/params" jujuversion "github.com/juju/juju/version" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/internal/dbmodel" @@ -24,7 +24,7 @@ var _ = gc.Suite(&clientSuite{}) func (s *clientSuite) TestStatus(c *gc.C) { ctx := context.Background() - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@external/pw1").String() + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@canonical.com/pw1").String() cred := jujuparams.TaggedCredential{ Tag: cct, Credential: jujuparams.CloudCredential{ @@ -53,7 +53,7 @@ func (s *clientSuite) TestStatus(c *gc.C) { var modelInfo jujuparams.ModelInfo err = s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "model-1", - OwnerTag: names.NewUserTag("bob@external").String(), + OwnerTag: names.NewUserTag("bob@canonical.com").String(), CloudCredentialTag: cct, }, &modelInfo) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuclient/cloud.go b/internal/jujuclient/cloud.go index 9ba3b695f..e14faca51 100644 --- a/internal/jujuclient/cloud.go +++ b/internal/jujuclient/cloud.go @@ -7,7 +7,7 @@ import ( jujuerrors "github.com/juju/errors" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" diff --git a/internal/jujuclient/cloud_test.go b/internal/jujuclient/cloud_test.go index dccfe163d..b88647397 100644 --- a/internal/jujuclient/cloud_test.go +++ b/internal/jujuclient/cloud_test.go @@ -6,7 +6,7 @@ import ( "strings" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" @@ -43,7 +43,7 @@ func (s *cloudSuite) TestCheckCredentialModels(c *gc.C) { func (s *cloudSuite) TestCheckCredentialModelsWithModels(c *gc.C) { ctx := context.Background() - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@external/pw1").String() + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@canonical.com/pw1").String() cred := jujuparams.TaggedCredential{ Tag: cct, Credential: jujuparams.CloudCredential{ @@ -62,7 +62,7 @@ func (s *cloudSuite) TestCheckCredentialModelsWithModels(c *gc.C) { var info jujuparams.ModelInfo err = s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "model-1", - OwnerTag: names.NewUserTag("bob@external").String(), + OwnerTag: names.NewUserTag("bob@canonical.com").String(), CloudCredentialTag: cct, }, &info) c.Assert(err, gc.Equals, nil) @@ -70,7 +70,7 @@ func (s *cloudSuite) TestCheckCredentialModelsWithModels(c *gc.C) { err = s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "model-2", - OwnerTag: names.NewUserTag("bob@external").String(), + OwnerTag: names.NewUserTag("bob@canonical.com").String(), CloudCredentialTag: cct, }, &info) c.Assert(err, gc.Equals, nil) @@ -151,7 +151,7 @@ func (s *cloudSuite) TestRevokeCredential(c *gc.C) { func (s *cloudSuite) TestUpdateCredentialWithModels(c *gc.C) { ctx := context.Background() - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@external/pw1").String() + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@canonical.com/pw1").String() cred := jujuparams.TaggedCredential{ Tag: cct, Credential: jujuparams.CloudCredential{ @@ -170,7 +170,7 @@ func (s *cloudSuite) TestUpdateCredentialWithModels(c *gc.C) { var info jujuparams.ModelInfo err = s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "model-1", - OwnerTag: names.NewUserTag("bob@external").String(), + OwnerTag: names.NewUserTag("bob@canonical.com").String(), CloudCredentialTag: cct, }, &info) c.Assert(err, gc.Equals, nil) @@ -178,7 +178,7 @@ func (s *cloudSuite) TestUpdateCredentialWithModels(c *gc.C) { err = s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "model-2", - OwnerTag: names.NewUserTag("bob@external").String(), + OwnerTag: names.NewUserTag("bob@canonical.com").String(), CloudCredentialTag: cct, }, &info) c.Assert(err, gc.Equals, nil) @@ -396,22 +396,22 @@ func (s *cloudSuite) TestRemoveCloud(c *gc.C) { } func (s *cloudSuite) TestGrantCloudAccess(c *gc.C) { - err := s.API.GrantCloudAccess(context.Background(), names.NewCloudTag("no-such-cloud"), names.NewUserTag("user@external"), "add-model") + err := s.API.GrantCloudAccess(context.Background(), names.NewCloudTag("no-such-cloud"), names.NewUserTag("user@canonical.com"), "add-model") c.Check(jujuparams.ErrCode(err), gc.Equals, jujuparams.CodeNotFound) - err = s.API.GrantCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), names.NewUserTag("user@external"), "add-model") + err = s.API.GrantCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), names.NewUserTag("user@canonical.com"), "add-model") c.Check(err, gc.Equals, nil) } func (s *cloudSuite) TestRevokeCloudAccess(c *gc.C) { - err := s.API.RevokeCloudAccess(context.Background(), names.NewCloudTag("no-such-cloud"), names.NewUserTag("user@external"), "add-model") + err := s.API.RevokeCloudAccess(context.Background(), names.NewCloudTag("no-such-cloud"), names.NewUserTag("user@canonical.com"), "add-model") c.Check(jujuparams.ErrCode(err), gc.Equals, jujuparams.CodeNotFound) - err = s.API.GrantCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), names.NewUserTag("user@external"), "admin") + err = s.API.GrantCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), names.NewUserTag("user@canonical.com"), "admin") c.Assert(err, gc.Equals, nil) - err = s.API.RevokeCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), names.NewUserTag("user@external"), "admin") + err = s.API.RevokeCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), names.NewUserTag("user@canonical.com"), "admin") c.Check(err, gc.Equals, nil) - err = s.API.RevokeCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), names.NewUserTag("user@external"), "add-model") + err = s.API.RevokeCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), names.NewUserTag("user@canonical.com"), "add-model") c.Check(err, gc.Equals, nil) - err = s.API.RevokeCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), names.NewUserTag("user@external"), "add-model") + err = s.API.RevokeCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), names.NewUserTag("user@canonical.com"), "add-model") c.Check(jujuparams.ErrCode(err), gc.Equals, jujuparams.CodeNotFound) } diff --git a/internal/jujuclient/dial.go b/internal/jujuclient/dial.go index 7589b4d36..887302e28 100644 --- a/internal/jujuclient/dial.go +++ b/internal/jujuclient/dial.go @@ -21,7 +21,7 @@ import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/juju/juju/api/base" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" "gopkg.in/httprequest.v1" @@ -199,6 +199,10 @@ func (c *Connection) IsBroken() bool { return c.client.IsBroken() } +func (c *Connection) RootHTTPClient() (*httprequest.Client, error) { + return c.HTTPClient() +} + // hasFacadeVersion returns whether the connection supports the given // facade at the given version. func (c *Connection) hasFacadeVersion(facade string, version int) bool { diff --git a/internal/jujuclient/dial_test.go b/internal/jujuclient/dial_test.go index 7ba5bc3f8..a6e1aa3b8 100644 --- a/internal/jujuclient/dial_test.go +++ b/internal/jujuclient/dial_test.go @@ -9,7 +9,7 @@ import ( "github.com/juju/juju/core/network" jujuparams "github.com/juju/juju/rpc/params" jujuversion "github.com/juju/juju/version" - "github.com/juju/names/v4" + "github.com/juju/names/v5" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" diff --git a/internal/jujuclient/modelmanager.go b/internal/jujuclient/modelmanager.go index b30af84ba..39140c66e 100644 --- a/internal/jujuclient/modelmanager.go +++ b/internal/jujuclient/modelmanager.go @@ -8,7 +8,7 @@ import ( jujuerrors "github.com/juju/errors" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/errors" ) diff --git a/internal/jujuclient/modelmanager_test.go b/internal/jujuclient/modelmanager_test.go index 9d8240f60..fddd319d9 100644 --- a/internal/jujuclient/modelmanager_test.go +++ b/internal/jujuclient/modelmanager_test.go @@ -10,7 +10,7 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/juju/juju/core/life" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" jc "github.com/juju/testing/checkers" "github.com/juju/utils/v2" gc "gopkg.in/check.v1" @@ -31,7 +31,7 @@ func (s *modelmanagerSuite) TestCreateModel(c *gc.C) { var info jujuparams.ModelInfo err := s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("test-user@external").String(), + OwnerTag: names.NewUserTag("test-user@canonical.com").String(), }, &info) c.Assert(err, gc.Equals, nil) @@ -53,7 +53,7 @@ func (s *modelmanagerSuite) TestCreateModelError(c *gc.C) { var info jujuparams.ModelInfo err := s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("test-user@external").String(), + OwnerTag: names.NewUserTag("test-user@canonical.com").String(), CloudTag: names.NewCloudTag("nosuchcloud").String(), }, &info) c.Check(jujuparams.ErrCode(err), gc.Equals, jujuparams.CodeNotFound) @@ -66,7 +66,7 @@ func (s *modelmanagerSuite) TestGrantJIMMModelAdmin(c *gc.C) { var info jujuparams.ModelInfo err := s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("test-user@external").String(), + OwnerTag: names.NewUserTag("test-user@canonical.com").String(), }, &info) c.Assert(err, gc.Equals, nil) @@ -99,7 +99,7 @@ func (s *modelmanagerSuite) TestModelInfo(c *gc.C) { var info jujuparams.ModelInfo err := s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("test-user@external").String(), + OwnerTag: names.NewUserTag("test-user@canonical.com").String(), }, &info) c.Assert(err, gc.Equals, nil) @@ -129,11 +129,11 @@ func (s *modelmanagerSuite) TestGrantRevokeModel(c *gc.C) { var info jujuparams.ModelInfo err := s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("test-user@external").String(), + OwnerTag: names.NewUserTag("test-user@canonical.com").String(), }, &info) c.Assert(err, gc.Equals, nil) - err = s.API.GrantModelAccess(ctx, names.NewModelTag(info.UUID), names.NewUserTag("test-user-2@external"), jujuparams.ModelReadAccess) + err = s.API.GrantModelAccess(ctx, names.NewModelTag(info.UUID), names.NewUserTag("test-user-2@canonical.com"), jujuparams.ModelReadAccess) c.Assert(err, gc.Equals, nil) err = s.API.ModelInfo(ctx, &info) @@ -143,22 +143,22 @@ func (s *modelmanagerSuite) TestGrantRevokeModel(c *gc.C) { return a.UserName < b.UserName } c.Check(info.Users, jimmtest.CmpEquals(cmpopts.SortSlices(lessf)), []jujuparams.ModelUserInfo{{ - UserName: "test-user@external", + UserName: "test-user@canonical.com", DisplayName: "test-user", Access: "admin", }, { - UserName: "test-user-2@external", + UserName: "test-user-2@canonical.com", Access: "read", }}) - err = s.API.RevokeModelAccess(ctx, names.NewModelTag(info.UUID), names.NewUserTag("test-user-2@external"), jujuparams.ModelReadAccess) + err = s.API.RevokeModelAccess(ctx, names.NewModelTag(info.UUID), names.NewUserTag("test-user-2@canonical.com"), jujuparams.ModelReadAccess) c.Assert(err, gc.Equals, nil) err = s.API.ModelInfo(ctx, &info) c.Assert(err, gc.Equals, nil) c.Check(info.Users, jimmtest.CmpEquals(cmpopts.SortSlices(lessf)), []jujuparams.ModelUserInfo{{ - UserName: "test-user@external", + UserName: "test-user@canonical.com", DisplayName: "test-user", Access: "admin", }}) @@ -188,7 +188,7 @@ func (s *modelmanagerSuite) TestValidateModelUpgrade(c *gc.C) { args := jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("test-user@external").String(), + OwnerTag: names.NewUserTag("test-user@canonical.com").String(), } var info jujuparams.ModelInfo @@ -209,7 +209,7 @@ func (s *modelmanagerSuite) TestDestroyModel(c *gc.C) { var info jujuparams.ModelInfo err := s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("test-user@external").String(), + OwnerTag: names.NewUserTag("test-user@canonical.com").String(), }, &info) c.Assert(err, gc.Equals, nil) @@ -228,7 +228,7 @@ func (s *modelmanagerSuite) TestModelStatus(c *gc.C) { var info jujuparams.ModelInfo err := s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("test-user@external").String(), + OwnerTag: names.NewUserTag("test-user@canonical.com").String(), }, &info) c.Assert(err, gc.Equals, nil) @@ -263,7 +263,7 @@ func (s *modelmanagerSuite) TestChangeModelCredential(c *gc.C) { var info jujuparams.ModelInfo err := s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "test-model", - OwnerTag: names.NewUserTag("test-user@external").String(), + OwnerTag: names.NewUserTag("test-user@canonical.com").String(), }, &info) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuclient/ping_test.go b/internal/jujuclient/ping_test.go index 54ab09c61..55479ff91 100644 --- a/internal/jujuclient/ping_test.go +++ b/internal/jujuclient/ping_test.go @@ -6,7 +6,7 @@ import ( "context" "github.com/canonical/jimm/internal/dbmodel" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" ) diff --git a/internal/jujuclient/storage.go b/internal/jujuclient/storage.go index ecbfae1c9..aef700dd2 100644 --- a/internal/jujuclient/storage.go +++ b/internal/jujuclient/storage.go @@ -7,7 +7,7 @@ import ( jujuerrors "github.com/juju/errors" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/errors" ) diff --git a/internal/jujuclient/storage_test.go b/internal/jujuclient/storage_test.go index 6f449bc64..8dde46c46 100644 --- a/internal/jujuclient/storage_test.go +++ b/internal/jujuclient/storage_test.go @@ -5,7 +5,7 @@ import ( "context" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/internal/dbmodel" @@ -21,7 +21,7 @@ var _ = gc.Suite(&storageSuite{}) func (s *storageSuite) TestListFilesystems(c *gc.C) { ctx := context.Background() - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@external/pw1") + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@canonical.com/pw1") cred := jujuparams.TaggedCredential{ Tag: cct.String(), Credential: jujuparams.CloudCredential{ @@ -50,7 +50,7 @@ func (s *storageSuite) TestListFilesystems(c *gc.C) { var modelInfo jujuparams.ModelInfo err = s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "model-1", - OwnerTag: names.NewUserTag("bob@external").String(), + OwnerTag: names.NewUserTag("bob@canonical.com").String(), CloudCredentialTag: cct.String(), }, &modelInfo) c.Assert(err, gc.Equals, nil) @@ -67,7 +67,7 @@ func (s *storageSuite) TestListFilesystems(c *gc.C) { func (s *storageSuite) TestListVolumes(c *gc.C) { ctx := context.Background() - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@external/pw1").String() + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@canonical.com/pw1").String() cred := jujuparams.TaggedCredential{ Tag: cct, Credential: jujuparams.CloudCredential{ @@ -96,7 +96,7 @@ func (s *storageSuite) TestListVolumes(c *gc.C) { var modelInfo jujuparams.ModelInfo err = s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "model-1", - OwnerTag: names.NewUserTag("bob@external").String(), + OwnerTag: names.NewUserTag("bob@canonical.com").String(), CloudCredentialTag: cct, }, &modelInfo) c.Assert(err, gc.Equals, nil) @@ -113,7 +113,7 @@ func (s *storageSuite) TestListVolumes(c *gc.C) { func (s *storageSuite) TestListStorageDetails(c *gc.C) { ctx := context.Background() - cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@external/pw1").String() + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@canonical.com/pw1").String() cred := jujuparams.TaggedCredential{ Tag: cct, Credential: jujuparams.CloudCredential{ @@ -142,7 +142,7 @@ func (s *storageSuite) TestListStorageDetails(c *gc.C) { var modelInfo jujuparams.ModelInfo err = s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ Name: "model-1", - OwnerTag: names.NewUserTag("bob@external").String(), + OwnerTag: names.NewUserTag("bob@canonical.com").String(), CloudCredentialTag: cct, }, &modelInfo) c.Assert(err, gc.Equals, nil) diff --git a/internal/openfga/names/names.go b/internal/openfga/names/names.go index 0843e2d17..606f4d485 100644 --- a/internal/openfga/names/names.go +++ b/internal/openfga/names/names.go @@ -12,7 +12,7 @@ import ( cofga "github.com/canonical/ofga" "github.com/juju/juju/core/permission" - "github.com/juju/names/v4" + "github.com/juju/names/v5" ) // Relation Types @@ -80,7 +80,7 @@ func ConvertTag[RT ResourceTagger](t RT) *Tag { if t.Kind() == names.UserTagKind && id == EveryoneUser { // A user with ID "*" represents "everyone" in OpenFGA and allows checks like // `user:bob reader type:my-resource` to return true without a separate query - // for the user:everyone@external user. + // for the user:everyone user. id = "*" } tag := &Tag{ diff --git a/internal/openfga/names/names_test.go b/internal/openfga/names/names_test.go index 566a8b9f1..5a2b2736a 100644 --- a/internal/openfga/names/names_test.go +++ b/internal/openfga/names/names_test.go @@ -5,7 +5,7 @@ package names_test import ( "testing" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" ofganames "github.com/canonical/jimm/internal/openfga/names" diff --git a/internal/openfga/openfga.go b/internal/openfga/openfga.go index b79c7fa98..c29330e2e 100644 --- a/internal/openfga/openfga.go +++ b/internal/openfga/openfga.go @@ -8,7 +8,7 @@ import ( "github.com/canonical/jimm/internal/errors" cofga "github.com/canonical/ofga" - "github.com/juju/names/v4" + "github.com/juju/names/v5" ofganames "github.com/canonical/jimm/internal/openfga/names" jimmnames "github.com/canonical/jimm/pkg/names" @@ -79,7 +79,7 @@ func NewOpenFGAClient(cofgaClient *cofga.Client) *OFGAClient { // publicAccessAdaptor handles cases where a tuple need to be transformed before being // returned to the application layer. The wildcard tuple * for users is replaced -// with the everyone@external user. +// with the everyone user. func publicAccessAdaptor(tt cofga.TimestampedTuple) cofga.TimestampedTuple { if tt.Tuple.Object.Kind == UserType && tt.Tuple.Object.IsPublicAccess() { tt.Tuple.Object.ID = ofganames.EveryoneUser diff --git a/internal/openfga/openfga_test.go b/internal/openfga/openfga_test.go index 6f9467740..f5dd8e479 100644 --- a/internal/openfga/openfga_test.go +++ b/internal/openfga/openfga_test.go @@ -10,7 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/internal/jimmtest" @@ -279,8 +279,8 @@ func (s *openFGATestSuite) TestRemoveApplicationOffer(c *gc.C) { func (s *openFGATestSuite) TestRemoveGroup(c *gc.C) { group1 := jimmnames.NewGroupTag("1") group2 := jimmnames.NewGroupTag("2") - alice := names.NewUserTag("alice@external") - adam := names.NewUserTag("adam@external") + alice := names.NewUserTag("alice@canonical.com") + adam := names.NewUserTag("adam@canonical.com") tuples := []openfga.Tuple{{ Object: ofganames.ConvertTag(alice), @@ -333,8 +333,8 @@ func (s *openFGATestSuite) TestRemoveGroup(c *gc.C) { func (s *openFGATestSuite) TestRemoveCloud(c *gc.C) { cloud1 := names.NewCloudTag("cloud-1") - alice := names.NewUserTag("alice@external") - adam := names.NewUserTag("adam@external") + alice := names.NewUserTag("alice@canonical.com") + adam := names.NewUserTag("adam@canonical.com") tuples := []openfga.Tuple{{ Object: ofganames.ConvertTag(alice), diff --git a/internal/openfga/user.go b/internal/openfga/user.go index dbb5e4c24..23b30dcc3 100644 --- a/internal/openfga/user.go +++ b/internal/openfga/user.go @@ -6,7 +6,7 @@ import ( "context" "strings" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" diff --git a/internal/openfga/user_test.go b/internal/openfga/user_test.go index 57e55df83..a4a843289 100644 --- a/internal/openfga/user_test.go +++ b/internal/openfga/user_test.go @@ -8,7 +8,7 @@ import ( cofga "github.com/canonical/ofga" "github.com/google/uuid" - "github.com/juju/names/v4" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/internal/dbmodel" diff --git a/internal/rpc/client_test.go b/internal/rpc/client_test.go index 96383013e..063c9b8df 100644 --- a/internal/rpc/client_test.go +++ b/internal/rpc/client_test.go @@ -16,7 +16,7 @@ import ( qt "github.com/frankban/quicktest" "github.com/gorilla/websocket" "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" diff --git a/internal/rpc/dial.go b/internal/rpc/dial.go index eb42057de..e043974a8 100644 --- a/internal/rpc/dial.go +++ b/internal/rpc/dial.go @@ -12,7 +12,7 @@ import ( "sync" "github.com/gorilla/websocket" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" diff --git a/internal/rpc/proxy.go b/internal/rpc/proxy.go index 2e02a841d..9b208e7bc 100644 --- a/internal/rpc/proxy.go +++ b/internal/rpc/proxy.go @@ -10,7 +10,7 @@ import ( "github.com/gorilla/websocket" "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" diff --git a/internal/vault/vault.go b/internal/vault/vault.go index dca403b65..a9e7c4cb0 100644 --- a/internal/vault/vault.go +++ b/internal/vault/vault.go @@ -12,7 +12,7 @@ import ( "time" "github.com/hashicorp/vault/api" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "github.com/lestrrat-go/jwx/v2/jwk" diff --git a/internal/vault/vault_test.go b/internal/vault/vault_test.go index 7ea280832..ab2f182e0 100644 --- a/internal/vault/vault_test.go +++ b/internal/vault/vault_test.go @@ -14,7 +14,7 @@ import ( qt "github.com/frankban/quicktest" "github.com/google/uuid" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" @@ -64,7 +64,7 @@ func TestVaultCloudCredentialAttributeStoreRoundTrip(t *testing.T) { st := newStore(c) ctx := context.Background() - tag := names.NewCloudCredentialTag("aws/alice@external/" + c.Name()) + tag := names.NewCloudCredentialTag("aws/alice@canonical.com/" + c.Name()) err := st.Put(ctx, tag, map[string]string{"a": "A", "b": "1234"}) c.Assert(err, qt.IsNil) @@ -84,7 +84,7 @@ func TestVaultCloudCredentialAtrributeStoreEmpty(t *testing.T) { st := newStore(c) ctx := context.Background() - tag := names.NewCloudCredentialTag("aws/alice@external/" + c.Name()) + tag := names.NewCloudCredentialTag("aws/alice@canonical.com/" + c.Name()) attr, err := st.Get(ctx, tag) c.Assert(err, qt.IsNil) diff --git a/local/candid/config.yaml b/local/candid/config.yaml deleted file mode 100644 index 5beede576..000000000 --- a/local/candid/config.yaml +++ /dev/null @@ -1,37 +0,0 @@ -storage: - type: postgres - connection-string: postgresql://jimm:jimm@db:5432/postgres?sslmode=disable -logging-config: "DEBUG" -auth-username: admin -auth-password: password -listen-address: 0.0.0.0:8081 -location: 'http://0.0.0.0:8081' -private-addr: "0.0.0.0" -private-key: 8PjzjakvIlh3BVFKe8axinRDutF6EDIfjtuf4+JaNow= -public-key: CIdWcEUN+0OZnKW9KwruRQnQDY/qqzVdD30CijwiWCk= -access-log: access.log -max-mgo-sessions: 300 -request-timeout: 2s -admin-agent-public-key: iKp24EL2Aj9MQfRkpwzp7rz7Zf3QZsEzWGpoWT3OK2w= -identity-providers: -- type: static - name: static - description: Default identity provider - domain: candid.localhost - users: - jimm: - name: JIMM User - email: jimm.user@example.com - password: jimm - groups: - - group1 - - group2 - joe: - name: Joe user (non superuser) - email: joe.user@example.com - password: joe - groups: - - group1 - - group2 - hidden: false - require-mfa: false diff --git a/local/candid/entry.sh b/local/candid/entry.sh deleted file mode 100755 index 42dc0dcfb..000000000 --- a/local/candid/entry.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -# This script is responsible for handling the CMD override of Candid in a local environment. - -echo "Entrypoint being overridden for local environment." - -apt update -apt install curl -y -exec /root/candidsrv /etc/candid/config.yaml diff --git a/local/seed_db/main.go b/local/seed_db/main.go index b3f16703f..65c44b52e 100644 --- a/local/seed_db/main.go +++ b/local/seed_db/main.go @@ -48,7 +48,7 @@ func main() { } u := dbmodel.Identity{ - Name: petname.Generate(2, "-") + "@external", + Name: petname.Generate(2, "-") + "@canonical.com", } if err = db.DB.Create(&u).Error; err != nil { fmt.Println("failed to add user to db ", err) diff --git a/local/traefik/certs/certs.sh b/local/traefik/certs/certs.sh index 5db4cef25..b9d004c1b 100755 --- a/local/traefik/certs/certs.sh +++ b/local/traefik/certs/certs.sh @@ -3,6 +3,13 @@ # A simple script to setup TLS for JIMM when running locally with compose. # Please make sure you run this in the ./certs directory. +if [ "$1" != "--force" ]; then + if [ -f "server.crt" ] && [ -f "server.key" ]; then + echo "Server certs already exist. Skipping cert generation." + echo "Run with --force to regenerate." + fi +fi + rm -f ca.crt ca.key server.key server.crt # Gen CA diff --git a/pkg/names/applicationoffer.go b/pkg/names/applicationoffer.go index 94f0f7474..9e3fa1da4 100644 --- a/pkg/names/applicationoffer.go +++ b/pkg/names/applicationoffer.go @@ -3,7 +3,7 @@ package names import ( "regexp" - "github.com/juju/names/v4" + "github.com/juju/names/v5" ) // JIMM handles applicationoffers via UUID, as such diff --git a/pkg/names/names.go b/pkg/names/names.go index 30631ac60..37ad1f661 100644 --- a/pkg/names/names.go +++ b/pkg/names/names.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/juju/names/v4" + "github.com/juju/names/v5" ) // TagKind returns one of the *TagKind constants for the given tag, or diff --git a/service.go b/service.go index 6fdb84d7f..3d152c5fd 100644 --- a/service.go +++ b/service.go @@ -5,7 +5,6 @@ package jimm import ( "context" "database/sql" - "encoding/json" "net/http" "net/url" "os" @@ -14,17 +13,11 @@ import ( "time" "github.com/antonlindstrom/pgstore" - "github.com/canonical/candid/candidclient" cofga "github.com/canonical/ofga" "github.com/go-chi/chi/v5" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dbrootkeystore" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" "github.com/google/uuid" vaultapi "github.com/hashicorp/vault/api" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" "gorm.io/driver/postgres" @@ -32,9 +25,9 @@ import ( "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/dashboard" - "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/debugapi" + "github.com/canonical/jimm/internal/discharger" "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/jimm" jimmcreds "github.com/canonical/jimm/internal/jimm/credentials" @@ -95,26 +88,10 @@ type Params struct { // will be used. DSN string - // CandidURL contains the URL of the candid server that the JIMM - // service will use for authentication. If this is empty then no - // authentication will be possible. - CandidURL string - - // CandidPublicKey contains the base64 encoded public key of the - // candid server specified in CandidURL. In most cases there is no - // need to set this parameter, The public key will be retrieved - // from the candid server itself. - CandidPublicKey string - - // BakeryAgentFile contains the path of a file containing agent - // authentication information for JIMM. If this is empty then - // authentication will only use information contained in the - // discharged macaroons. - BakeryAgentFile string - - // ControllerAdmins contains a list of candid users (or groups) + // ControllerAdmins contains a list of users (or groups) // that will be given the access-level "superuser" when they // authenticate to the controller. + // TODO(CSS-7507) - Wire this up for OAuth bootstrapping. ControllerAdmins []string // DisableConnectionCache disables caching connections to @@ -252,6 +229,8 @@ func NewService(ctx context.Context, p Params) (*Service, error) { s := new(Service) s.mux = chi.NewRouter() + // Setup all dependency services + if p.ControllerUUID == "" { controllerUUID, err := uuid.NewRandom() if err != nil { @@ -311,20 +290,6 @@ func NewService(ctx context.Context, p Params) (*Service, error) { return nil, errors.E(op, err, "failed to ensure controller admins") } - kp, dischargeMux, err := s.setupDischarger(p, openFGAclient) - if err != nil { - return nil, errors.E(op, err, "failed to set up discharger") - } - s.mux.Handle(localDischargePath+"/*", dischargeMux) - - // Ale8k: This authenticator is old and used for macaroon auth - // it is still present for backwards compatibility but SHOULD - // be removed in the future. - s.jimm.Authenticator, err = newAuthenticator(ctx, &s.jimm.Database, openFGAclient, kp, p) - if err != nil { - return nil, errors.E(op, err) - } - authSvc, err := auth.NewAuthenticationService( ctx, auth.AuthenticationServiceParams{ @@ -368,6 +333,7 @@ func NewService(ctx context.Context, p Params) (*Service, error) { return nil, errors.E(op, err, "failed to parse final redirect url for the dashboard") } + // Setup all HTTP handlers. mountHandler := func(path string, h jimmhttp.JIMMHttpHandler) { s.mux.Mount(path, h.Routes()) } @@ -398,11 +364,15 @@ func NewService(ctx context.Context, p Params) (*Service, error) { "/auth", oauthHandler, ) + macaroonDischarger, err := s.setupDischarger(p) + if err != nil { + return nil, errors.E(op, err, "failed to set up discharger") + } + s.mux.Handle(localDischargePath+"/*", discharger.GetDischargerMux(macaroonDischarger, localDischargePath)) params := jujuapi.Params{ - ControllerUUID: p.ControllerUUID, - IdentityLocation: p.CandidURL, - PublicDNSName: p.PublicDNSName, + ControllerUUID: p.ControllerUUID, + PublicDNSName: p.PublicDNSName, } s.mux.Handle("/api", jujuapi.APIHandler(ctx, &s.jimm, params)) @@ -419,22 +389,18 @@ func NewService(ctx context.Context, p Params) (*Service, error) { // setupDischarger set JIMM up as a discharger of 3rd party caveats addressed to it. This is intended // to enable Juju controllers to check for permissions using a macaroon-based workflow (atm only // for cross model relations). -func (s *Service) setupDischarger(p Params, openFGAclient *openfga.OFGAClient) (*bakery.KeyPair, *http.ServeMux, error) { - macaroonDischarger, err := newMacaroonDischarger(p, &s.jimm.Database, openFGAclient) +func (s *Service) setupDischarger(p Params) (*discharger.MacaroonDischarger, error) { + cfg := discharger.MacaroonDischargerConfig{ + PublicKey: p.PublicKey, + PrivateKey: p.PrivateKey, + MacaroonExpiryDuration: p.MacaroonExpiryDuration, + ControllerUUID: p.ControllerUUID, + } + MacaroonDischarger, err := discharger.NewMacaroonDischarger(cfg, &s.jimm.Database, s.jimm.OpenFGAClient) if err != nil { - return nil, nil, errors.E(err) + return nil, errors.E(err) } - - discharger := httpbakery.NewDischarger( - httpbakery.DischargerParams{ - Key: &macaroonDischarger.kp, - Checker: httpbakery.ThirdPartyCaveatCheckerFunc(macaroonDischarger.checkThirdPartyCaveat), - }, - ) - dischargeMux := http.NewServeMux() - discharger.AddMuxHandlers(dischargeMux, localDischargePath) - - return &macaroonDischarger.kp, dischargeMux, nil + return MacaroonDischarger, nil } func setupSessionStore(db *sql.DB, secretKey string) (*pgstore.PGStore, error) { @@ -463,81 +429,6 @@ func openDB(ctx context.Context, dsn string) (*gorm.DB, error) { }) } -func newAuthenticator(ctx context.Context, db *db.Database, client *openfga.OFGAClient, key *bakery.KeyPair, p Params) (jimm.Authenticator, error) { - if p.CandidURL == "" { - // No authenticator configured - return nil, nil - } - zapctx.Info(ctx, "configuring authenticator", - zap.String("CandidURL", p.CandidURL), - zap.String("CandidPublicKey", p.CandidPublicKey), - zap.String("BakeryAgentFile", p.BakeryAgentFile), - ) - tps := bakery.NewThirdPartyStore() - if p.CandidPublicKey != "" { - var pk bakery.PublicKey - if err := pk.Key.UnmarshalText([]byte(p.CandidPublicKey)); err != nil { - return nil, err - } - tps.AddInfo(p.CandidURL, bakery.ThirdPartyInfo{ - PublicKey: pk, - Version: bakery.Version2, - }) - } - - bClient := httpbakery.NewClient() - var agentUsername string - if p.BakeryAgentFile != "" { - data, err := os.ReadFile(p.BakeryAgentFile) - if err != nil { - return nil, err - } - var info agent.AuthInfo - if err := json.Unmarshal(data, &info); err != nil { - return nil, err - } - if err := agent.SetUpAuth(bClient, &info); err != nil { - return nil, err - } - for _, a := range info.Agents { - if a.URL == p.CandidURL { - agentUsername = a.Username - } - } - } - candidClient, err := candidclient.New(candidclient.NewParams{ - BaseURL: p.CandidURL, - Client: bClient, - AgentUsername: agentUsername, - CacheTime: 10 * time.Minute, - }) - if err != nil { - return nil, err - } - - if p.MacaroonExpiryDuration == 0 { - p.MacaroonExpiryDuration = 24 * time.Hour - } - - return auth.JujuAuthenticator{ - Bakery: identchecker.NewBakery(identchecker.BakeryParams{ - RootKeyStore: dbrootkeystore.NewRootKeys(100, nil).NewStore( - db, - dbrootkeystore.Policy{ - ExpiryDuration: p.MacaroonExpiryDuration, - }, - ), - Locator: httpbakery.NewThirdPartyLocator(nil, tps), - Key: key, - IdentityClient: candidClient, - Location: "jimm", - Logger: logger.BakeryLogger{}, - }), - ControllerAdmins: p.ControllerAdmins, - Client: client, - }, nil -} - func (s *Service) setupCredentialStore(ctx context.Context, p Params) error { const op = errors.Op("newSecretStore") vs, err := newVaultStore(ctx, p) diff --git a/service_test.go b/service_test.go index e019db7ad..5fdd9fe8c 100644 --- a/service_test.go +++ b/service_test.go @@ -4,29 +4,25 @@ package jimm_test import ( "context" - "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "os" - "path/filepath" "testing" "time" - "github.com/canonical/candid/candidtest" cofga "github.com/canonical/ofga" "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" - "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" "github.com/juju/juju/api" "github.com/juju/juju/api/client/cloud" jujucloud "github.com/juju/juju/cloud" "github.com/juju/juju/core/macaroon" - "github.com/juju/names/v4" + "github.com/juju/names/v5" "github.com/canonical/jimm" "github.com/canonical/jimm/internal/dbmodel" @@ -106,7 +102,6 @@ func TestAuthenticator(t *testing.T) { }, DashboardFinalRedirectURL: "", } - candid := startCandid(c, &p) svc, err := jimm.NewService(context.Background(), p) c.Assert(err, qt.IsNil) @@ -117,7 +112,7 @@ func TestAuthenticator(t *testing.T) { } conn, err := api.Open(&info, api.DialOpts{ - BakeryClient: userClient(candid, "alice", "admin"), + LoginProvider: jimmtest.NewUserSessionLogin("alice"), InsecureSkipVerify: true, }) c.Assert(err, qt.IsNil) @@ -128,11 +123,11 @@ func TestAuthenticator(t *testing.T) { }) c.Check(conn.ControllerTag(), qt.Equals, names.NewControllerTag("6acf4fd8-32d6-49ea-b4eb-dcb9d1590c11")) - c.Check(conn.AuthTag(), qt.Equals, names.NewUserTag("alice@external")) + c.Check(conn.AuthTag(), qt.Equals, names.NewUserTag("alice@canonical.com")) c.Check(conn.ControllerAccess(), qt.Equals, "") conn, err = api.Open(&info, api.DialOpts{ - BakeryClient: userClient(candid, "bob"), + LoginProvider: jimmtest.NewUserSessionLogin("bob"), InsecureSkipVerify: true, }) c.Assert(err, qt.IsNil) @@ -143,7 +138,7 @@ func TestAuthenticator(t *testing.T) { }) c.Check(conn.ControllerTag(), qt.Equals, names.NewControllerTag("6acf4fd8-32d6-49ea-b4eb-dcb9d1590c11")) - c.Check(conn.AuthTag(), qt.Equals, names.NewUserTag("bob@external")) + c.Check(conn.AuthTag(), qt.Equals, names.NewUserTag("bob@canonical.com")) c.Check(conn.ControllerAccess(), qt.Equals, "") } @@ -176,7 +171,6 @@ func TestVault(t *testing.T) { }, DashboardFinalRedirectURL: "", } - candid := startCandid(c, &p) vaultClient, _, creds, _ := jimmtest.VaultClient(c, ".") svc, err := jimm.NewService(context.Background(), p) @@ -192,7 +186,7 @@ func TestVault(t *testing.T) { } conn, err := api.Open(&info, api.DialOpts{ - BakeryClient: userClient(candid, "bob"), + LoginProvider: jimmtest.NewUserSessionLogin("bob"), InsecureSkipVerify: true, }) c.Assert(err, qt.IsNil) @@ -204,7 +198,7 @@ func TestVault(t *testing.T) { cloudClient := cloud.NewClient(conn) - tag := names.NewCloudCredentialTag("test/bob@external/test-1").String() + tag := names.NewCloudCredentialTag("test/bob@canonical.com/test-1").String() _, err = cloudClient.UpdateCloudsCredentials(map[string]jujucloud.Credential{ tag: jujucloud.NewCredential(jujucloud.UserPassAuthType, map[string]string{ "username": "test-user", @@ -219,7 +213,7 @@ func TestVault(t *testing.T) { AuthPath: p.VaultAuthPath, KVPath: p.VaultPath, } - attr, err := store.Get(context.Background(), names.NewCloudCredentialTag("test/bob@external/test-1")) + attr, err := store.Get(context.Background(), names.NewCloudCredentialTag("test/bob@canonical.com/test-1")) c.Assert(err, qt.IsNil) c.Check(attr, qt.DeepEquals, map[string]string{ "username": "test-user", @@ -269,7 +263,6 @@ func TestOpenFGA(t *testing.T) { }, DashboardFinalRedirectURL: "", } - candid := startCandid(c, &p) svc, err := jimm.NewService(context.Background(), p) c.Assert(err, qt.IsNil) @@ -280,7 +273,7 @@ func TestOpenFGA(t *testing.T) { } conn, err := api.Open(&info, api.DialOpts{ - BakeryClient: userClient(candid, "bob"), + LoginProvider: jimmtest.NewUserSessionLogin("bob"), InsecureSkipVerify: true, }) c.Assert(err, qt.IsNil) @@ -326,14 +319,13 @@ func TestPublicKey(t *testing.T) { }, DashboardFinalRedirectURL: "", } - _ = startCandid(c, &p) svc, err := jimm.NewService(context.Background(), p) c.Assert(err, qt.IsNil) srv := httptest.NewTLSServer(svc) c.Cleanup(srv.Close) - response, err := http.Get(srv.URL + "/macaroons/publickey") + response, err := srv.Client().Get(srv.URL + "/macaroons/publickey") c.Assert(err, qt.IsNil) data, err := io.ReadAll(response.Body) c.Assert(err, qt.IsNil) @@ -348,7 +340,7 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { Name: "test-application-offer", } user := dbmodel.Identity{ - Name: "alice@external", + Name: "alice@canonical.com", } ctx := context.Background() @@ -415,7 +407,6 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { }, DashboardFinalRedirectURL: "", } - _ = startCandid(c, &p) svc, err := jimm.NewService(context.Background(), p) c.Assert(err, qt.IsNil) @@ -456,6 +447,9 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { } bakeryClient := httpbakery.NewClient() + // Give the bakery client the transport config from the test server client + // so that the bakery client has the necessary certs for the test server. + bakeryClient.Client.Transport = srv.Client().Transport ms, err := bakeryClient.DischargeAll(context.TODO(), m) if test.expectedError != "" { c.Assert(err, qt.ErrorMatches, test.expectedError) @@ -471,53 +465,6 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { } } -func startCandid(c *qt.C, p *jimm.Params) *candidtest.Server { - candid := candidtest.NewServer() - c.Cleanup(candid.Close) - p.CandidURL = candid.URL.String() - - tpi, err := httpbakery.ThirdPartyInfoForLocation(context.Background(), nil, candid.URL.String()) - c.Assert(err, qt.IsNil) - pk, err := tpi.PublicKey.MarshalText() - c.Assert(err, qt.IsNil) - p.CandidPublicKey = string(pk) - - candid.AddUser("jimm-agent", candidtest.GroupListGroup) - buf, err := json.Marshal(agent.AuthInfo{ - Key: key(candid, "jimm-agent"), - Agents: []agent.Agent{{ - URL: candid.URL.String(), - Username: "jimm-agent", - }}, - }) - c.Assert(err, qt.IsNil) - p.BakeryAgentFile = filepath.Join(c.TempDir(), "agent.json") - err = os.WriteFile(p.BakeryAgentFile, buf, 0400) - c.Assert(err, qt.IsNil) - return candid -} - -func userClient(candid *candidtest.Server, user string, groups ...string) *httpbakery.Client { - candid.AddUser(user, groups...) - client := httpbakery.NewClient() - agent.SetUpAuth(client, &agent.AuthInfo{ - Key: key(candid, user), - Agents: []agent.Agent{{ - URL: candid.URL.String(), - Username: user, - }}, - }) - return client -} - -func key(candid *candidtest.Server, user string) *bakery.KeyPair { - key := candid.UserPublicKey(user) - return &bakery.KeyPair{ - Public: bakery.PublicKey{Key: bakery.Key(key.Public.Key)}, - Private: bakery.PrivateKey{Key: bakery.Key(key.Private.Key)}, - } -} - // cofgaParamsToJIMMOpenFGAParams To avoid circular references, the test setup function (jimmtest.SetupTestOFGAClient) // does not provide us with an instance of `jimm.OpenFGAParams`, so it just returns a `cofga.OpenFGAParams` instance. // This method reshapes the later into the former. From 7fc476c126195905b0ae379c3a4d862fcda321c9 Mon Sep 17 00:00:00 2001 From: ale8k Date: Mon, 18 Mar 2024 08:57:04 +0000 Subject: [PATCH 075/126] WIP browser cookie sessions --- internal/auth/oauth2.go | 174 ++++++++++++++++++++++- internal/auth/oauth2_test.go | 8 ++ internal/dbmodel/identity.go | 7 + internal/dbmodel/sql/postgres/1_6.sql | 2 + internal/jimm/jimm.go | 10 +- internal/jimm/user_test.go | 7 + internal/jimmhttp/auth_handler.go | 38 ++--- internal/jimmhttp/auth_handler_test.go | 140 +++--------------- internal/jimmhttp/websocket.go | 33 +++++ internal/jimmhttp/websocket_test.go | 11 ++ internal/jimmtest/auth.go | 156 ++++++++++++++++++++ internal/jimmtest/suite.go | 4 +- internal/jujuapi/admin.go | 34 +++++ internal/jujuapi/admin_test.go | 117 ++++++++++++++- internal/jujuapi/controllerroot.go | 8 +- internal/jujuapi/export_test.go | 2 +- internal/jujuapi/pinger_internal_test.go | 2 +- internal/jujuapi/websocket.go | 16 ++- internal/jujuapi/websocket_test.go | 32 ++++- service.go | 4 +- 20 files changed, 639 insertions(+), 166 deletions(-) diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index c98b7e802..d532f2d09 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -12,11 +12,13 @@ import ( "context" "encoding/base64" stderrors "errors" + "net/http" "net/mail" "strings" "time" "github.com/coreos/go-oidc/v3/oidc" + "github.com/gorilla/sessions" "github.com/juju/zaputil/zapctx" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwt" @@ -28,6 +30,29 @@ import ( "github.com/canonical/jimm/internal/errors" ) +const ( + // SessionName is the name of the gorilla session and is used to retrieve + // the session object from the database. + SessionName = "jimm-browser-session" + + // SessionIdentityKey is the key for the identity value stored within the + // session. + SessionIdentityKey = "identity-id" +) + +type sessionIdentityContextKey struct{} + +func ContextWithSessionIdentity(ctx context.Context, sessionIdentityId any) context.Context { + return context.WithValue(ctx, sessionIdentityContextKey{}, sessionIdentityId) +} +func SessionIdentityFromContext(ctx context.Context) string { + v := ctx.Value(sessionIdentityContextKey{}) + if v == nil { + return "" + } + return v.(string) +} + // AuthenticationService handles authentication within JIMM. type AuthenticationService struct { oauthConfig oauth2.Config @@ -37,7 +62,12 @@ type AuthenticationService struct { // sessionTokenExpiry holds the expiry time for JIMM minted session tokens (JWTs). sessionTokenExpiry time.Duration + // sessionCookieMaxAge holds the max age for session cookies. + sessionCookieMaxAge int + db IdentityStore + + sessionStore sessions.Store } // Identity store holds the necessary methods to get and update an identity @@ -62,6 +92,8 @@ type AuthenticationServiceParams struct { Scopes []string // SessionTokenExpiry holds the expiry time of minted JIMM session tokens (JWTs). SessionTokenExpiry time.Duration + // sessionCookieMaxAge holds the max age for session cookies. + SessionCookieMaxAge int // RedirectURL is the URL for handling the exchange of authorisation // codes into access tokens (and id tokens), for JIMM, this is expected // to be the servers own callback endpoint registered under /auth/callback. @@ -71,6 +103,9 @@ type AuthenticationServiceParams struct { // to fetch and update identities. I.e., their access tokens, refresh tokens, // display name, etc. Store IdentityStore + + // SessionStore holds the store for creating, getting and saving gorrila sessions. + SessionStore sessions.Store } // NewAuthenticationService returns a new authentication service for handling @@ -93,8 +128,10 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP Scopes: params.Scopes, RedirectURL: params.RedirectURL, }, - sessionTokenExpiry: params.SessionTokenExpiry, - db: params.Store, + sessionTokenExpiry: params.SessionTokenExpiry, + db: params.Store, + sessionStore: params.SessionStore, + sessionCookieMaxAge: params.SessionCookieMaxAge, }, nil } @@ -277,6 +314,8 @@ func (as *AuthenticationService) UpdateIdentity(ctx context.Context, email strin u.AccessToken = token.AccessToken u.RefreshToken = token.RefreshToken + u.AccessTokenExpiry = token.Expiry + u.AccessTokenType = token.TokenType if err := db.UpdateIdentity(ctx, u); err != nil { return errors.E(op, err) } @@ -335,3 +374,134 @@ func (as *AuthenticationService) VerifyClientCredentials(ctx context.Context, cl } return nil } + +// CreateBrowserSession creates a session and updates the cookie for a browser +// login callback. +func (as *AuthenticationService) CreateBrowserSession( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + secureCookies bool, + email string, +) error { + const op = errors.Op("") + + session, err := as.sessionStore.Get(r, SessionName) + if err != nil { + return errors.E(op, err) + } + + session.IsNew = true // Sets cookie to a fresh new cookie + session.Options.MaxAge = as.sessionCookieMaxAge // Expiry in seconds + session.Options.Secure = secureCookies // Ensures only sent with HTTPS + session.Options.HttpOnly = false // Allow Javascript to read it + + session.Values[SessionIdentityKey] = email + if err = session.Save(r, w); err != nil { + return errors.E(op, err) + } + return nil +} + +// AuthenticateBrowserSession updates the session for a browser, additionally +// retrieving new access tokens upon expiry. If this cannot be done, the cookie +// is deleted and an error is returned. +func (as *AuthenticationService) AuthenticateBrowserSession(ctx context.Context, w http.ResponseWriter, req *http.Request) (context.Context, error) { + const op = errors.Op("") + + // Get the session for this cookie + session, err := as.sessionStore.Get(req, SessionName) + if err != nil { + return ctx, errors.E(op, err) + } + + // Get the identity id (email) + identityId := session.Values[SessionIdentityKey] + + // Check the access token is ok + err = as.validateAndUpdateAccessToken(ctx, identityId) + + // If it's not ok, kill their session + if err != nil { + session.Options.MaxAge = -1 + if err = session.Save(req, w); err != nil { + return ctx, errors.E(op, err) + } + return ctx, errors.E(op, err) + } + // Otherwise update the context with the identity id + ctx = ContextWithSessionIdentity(ctx, identityId) + + // Extend the session + session.Options.MaxAge = as.sessionCookieMaxAge + if err = session.Save(req, w); err != nil { + return ctx, errors.E(op, err) + } + + // And give the context back with the identity id present + return ctx, nil +} + +// validateAndUpdateAccessToken +func (as *AuthenticationService) validateAndUpdateAccessToken(ctx context.Context, email any) error { + const op = errors.Op("") + + // Cast the email, it is any because we pass it through the context when authenticating + // with cookies and it makes sense to handle the casting here + emailStr, ok := email.(string) + if !ok { + return errors.E(op, "failed to cast email") + } + + // Get identity + db := as.db + u := &dbmodel.Identity{ + Name: emailStr, + } + if err := db.GetIdentity(ctx, u); err != nil { + return errors.E(op, err) + } + + // Construct token + t := &oauth2.Token{ + AccessToken: u.AccessToken, + RefreshToken: u.RefreshToken, + Expiry: u.AccessTokenExpiry, + TokenType: u.AccessTokenType, + } + + // Check its valid + if t.Valid() { + return nil + } + + // Attempt to update the identity with a new token + if err := as.refreshIdentitiesToken(ctx, emailStr, t); err != nil { + return errors.E(op, err) + } + + // All good! + return nil +} + +// refreshIdentitiesToken creates a token source based on the expired token and performs +// a manual token refresh, updating the identity afterwards. +// +// This is to be called only when a token is expired. +func (as *AuthenticationService) refreshIdentitiesToken(ctx context.Context, email string, t *oauth2.Token) error { + const op = errors.Op("") + + tSrc := as.oauthConfig.TokenSource(ctx, t) + + // Get a new access and refresh token + newToken, err := tSrc.Token() + if err != nil { + return errors.E(op, err, "failed to refresh token") + } + + if err := as.UpdateIdentity(ctx, email, newToken); err != nil { + return errors.E(op, err, "failed to update identity") + } + + return nil +} diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index d07e9e6e6..96e5c219e 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + "github.com/antonlindstrom/pgstore" "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" @@ -27,6 +28,12 @@ func setupTestAuthSvc(ctx context.Context, c *qt.C, expiry time.Duration) (*auth } c.Assert(db.Migrate(ctx, false), qt.IsNil) + sqldb, err := db.DB.DB() + c.Assert(err, qt.IsNil) + + sessionStore, err := pgstore.NewPGStoreFromPool(sqldb, []byte("secretsecretdigletts")) + c.Assert(err, qt.IsNil) + authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ IssuerURL: "http://localhost:8082/realms/jimm", ClientID: "jimm-device", @@ -35,6 +42,7 @@ func setupTestAuthSvc(ctx context.Context, c *qt.C, expiry time.Duration) (*auth SessionTokenExpiry: expiry, RedirectURL: "http://localhost:8080/auth/callback", Store: db, + SessionStore: sessionStore, }) c.Assert(err, qt.IsNil) diff --git a/internal/dbmodel/identity.go b/internal/dbmodel/identity.go index 75607fc65..1888af930 100644 --- a/internal/dbmodel/identity.go +++ b/internal/dbmodel/identity.go @@ -4,6 +4,7 @@ package dbmodel import ( "database/sql" + "time" jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" @@ -44,6 +45,12 @@ type Identity struct { // from the browser or device flow, and as such is updated on every successful // login. RefreshToken string + + // AccessTokenExpiry is the expiration date for this access token. + AccessTokenExpiry time.Time + + // AccessTokenType is the type for the token, typically bearer. + AccessTokenType string } // Tag returns a names.Tag for the identity. diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql index d5ba10f6d..5f380c483 100644 --- a/internal/dbmodel/sql/postgres/1_6.sql +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -2,6 +2,8 @@ -- and is a migration that renames `user` to `identity`. ALTER TABLE users ADD COLUMN access_token TEXT; ALTER TABLE users ADD COLUMN refresh_token TEXT; +ALTER TABLE users ADD COLUMN access_token_expiry TIMESTAMP; +ALTER TABLE users ADD COLUMN access_token_type TEXT; -- Note that we don't need to rename underlying indexes/constraints. As Postgres -- docs states: diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index af6ce42f3..2f8ce6301 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -7,10 +7,10 @@ package jimm import ( "context" "database/sql" + "net/http" "strings" "time" - "github.com/antonlindstrom/pgstore" "github.com/coreos/go-oidc/v3/oidc" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/juju/api/base" @@ -85,9 +85,6 @@ type JIMM struct { // OAuthAuthenticator is responsible for handling authentication // via OAuth2.0 AND JWT access tokens to JIMM. OAuthAuthenticator OAuthAuthenticator - - // CookieSessionStore is respnsible for handling cookie based sessions. - CookieSessionStore *pgstore.PGStore } // OAuthAuthenticationService returns the JIMM's authentication service. @@ -171,6 +168,11 @@ type OAuthAuthenticator interface { // VerifyClientCredentials verifies the provided client ID and client secret. VerifyClientCredentials(ctx context.Context, clientID string, clientSecret string) error + + // AuthenticateBrowserSession updates the session for a browser, additionally + // retrieving new access tokens upon expiry. If this cannot be done, the cookie + // is deleted and an error is returned. + AuthenticateBrowserSession(ctx context.Context, w http.ResponseWriter, req *http.Request) (context.Context, error) } type permission struct { diff --git a/internal/jimm/user_test.go b/internal/jimm/user_test.go index 1e55c516d..6dd6ed31a 100644 --- a/internal/jimm/user_test.go +++ b/internal/jimm/user_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/antonlindstrom/pgstore" qt "github.com/frankban/quicktest" "github.com/juju/names/v5" @@ -29,6 +30,11 @@ func TestGetOpenFGAUser(t *testing.T) { db := &db.Database{ DB: jimmtest.PostgresDB(c, func() time.Time { return time.Now() }), } + sqldb, err := db.DB.DB() + c.Assert(err, qt.IsNil) + + sessionStore, err := pgstore.NewPGStoreFromPool(sqldb, []byte("secretsecretdigletts")) + c.Assert(err, qt.IsNil) // TODO(ale8k): Mock this authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ IssuerURL: "http://localhost:8082/realms/jimm", @@ -36,6 +42,7 @@ func TestGetOpenFGAUser(t *testing.T) { Scopes: []string{"openid", "profile", "email"}, SessionTokenExpiry: time.Hour, Store: db, + SessionStore: sessionStore, }) c.Assert(err, qt.IsNil) diff --git a/internal/jimmhttp/auth_handler.go b/internal/jimmhttp/auth_handler.go index ac33cc379..461304d63 100644 --- a/internal/jimmhttp/auth_handler.go +++ b/internal/jimmhttp/auth_handler.go @@ -4,7 +4,6 @@ import ( "context" "net/http" - "github.com/antonlindstrom/pgstore" "github.com/coreos/go-oidc/v3/oidc" "github.com/go-chi/chi/v5" "github.com/juju/zaputil/zapctx" @@ -20,7 +19,6 @@ type OAuthHandler struct { Router *chi.Mux authenticator BrowserOAuthAuthenticator dashboardFinalRedirectURL string - sessionStore *pgstore.PGStore secureCookies bool cookieExpiry int } @@ -34,9 +32,6 @@ type OAuthHandlerParams struct { // upon completing the authorisation code flow. DashboardFinalRedirectURL string - // SessionStore is the cookie session store. - SessionStore *pgstore.PGStore - // SessionCookies determines if HTTPS must be enabled in order for JIMM // to set cookies when creating browser based sessions. SecureCookies bool @@ -53,6 +48,13 @@ type BrowserOAuthAuthenticator interface { ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) Email(idToken *oidc.IDToken) (string, error) UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error + CreateBrowserSession( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + secureCookies bool, + email string, + ) error } // NewOAuthHandler returns a new OAuth handler. @@ -63,14 +65,10 @@ func NewOAuthHandler(p OAuthHandlerParams) (*OAuthHandler, error) { if p.DashboardFinalRedirectURL == "" { return nil, errors.E("final redirect url not specified") } - if p.SessionStore == nil { - return nil, errors.E("nil session store") - } return &OAuthHandler{ Router: chi.NewRouter(), authenticator: p.Authenticator, dashboardFinalRedirectURL: p.DashboardFinalRedirectURL, - sessionStore: p.SessionStore, secureCookies: p.SecureCookies, cookieExpiry: p.CookieExpiry, }, nil @@ -129,22 +127,16 @@ func (oah *OAuthHandler) Callback(w http.ResponseWriter, r *http.Request) { return } - // If the session is empty, it'll just be an empty session, we only check - // errors for bad decoding etc. - session, err := oah.sessionStore.Get(r, "jimm-browser-session") - if err != nil { - writeError(ctx, w, http.StatusBadRequest, err, "failed to get session") + if err := oah.authenticator.CreateBrowserSession( + ctx, + w, + r, + oah.secureCookies, + email, + ); err != nil { + writeError(ctx, w, http.StatusBadRequest, err, "failed to setup session") } - session.IsNew = true // Sets cookie to a fresh new cookie - session.Options.MaxAge = oah.cookieExpiry // Expiry in seconds - session.Options.Secure = oah.secureCookies // Ensures only sent with HTTPS - session.Options.HttpOnly = false // Allow Javascript to read it - - session.Values["jimm-session"] = email - if err = session.Save(r, w); err != nil { - writeError(ctx, w, http.StatusBadRequest, err, "failed to save session") - } http.Redirect(w, r, oah.dashboardFinalRedirectURL, http.StatusPermanentRedirect) } diff --git a/internal/jimmhttp/auth_handler_test.go b/internal/jimmhttp/auth_handler_test.go index 849623985..d45954264 100644 --- a/internal/jimmhttp/auth_handler_test.go +++ b/internal/jimmhttp/auth_handler_test.go @@ -2,30 +2,20 @@ package jimmhttp_test import ( "context" - "fmt" "io" - "math/rand" - "net" "net/http" - "net/http/cookiejar" - "net/http/httptest" - "net/url" - "regexp" - "strconv" "testing" "time" "github.com/antonlindstrom/pgstore" - "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" + "github.com/gorilla/sessions" - "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/db" - "github.com/canonical/jimm/internal/jimmhttp" "github.com/canonical/jimm/internal/jimmtest" ) -func setupDbAndSessionStore(c *qt.C) (*db.Database, *pgstore.PGStore) { +func setupDbAndSessionStore(c *qt.C) (*db.Database, sessions.Store) { // Setup db ahead of time so we have access to session store db := &db.Database{ DB: jimmtest.PostgresDB(c, func() time.Time { return time.Now() }), @@ -41,52 +31,6 @@ func setupDbAndSessionStore(c *qt.C) (*db.Database, *pgstore.PGStore) { return db, store } -func setupTestServer(c *qt.C, dashboardURL string, db *db.Database, sessionStore *pgstore.PGStore) *httptest.Server { - // Create unstarted server to enable auth service - s := httptest.NewUnstartedServer(nil) - // Setup random port listener - minPort := 30000 - maxPort := 50000 - - port := strconv.Itoa(rand.Intn(maxPort-minPort+1) + minPort) - l, err := net.Listen("tcp", "localhost:"+port) - c.Assert(err, qt.IsNil) - // Set the listener with a random port - s.Listener = l - - // Remember redirect url to check it matches after test server starts - redirectURL := "http://127.0.0.1:" + port + "/callback" - authSvc, err := auth.NewAuthenticationService(context.Background(), auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Hour, - // Now we know the port the test server is running on - RedirectURL: redirectURL, - Store: db, - }) - c.Assert(err, qt.IsNil) - - h, err := jimmhttp.NewOAuthHandler(jimmhttp.OAuthHandlerParams{ - Authenticator: authSvc, - DashboardFinalRedirectURL: dashboardURL, - SessionStore: sessionStore, - SecureCookies: false, - CookieExpiry: 86400, - }) - c.Assert(err, qt.IsNil) - - s.Config.Handler = h.Routes() - - s.Start() - - // Ensure redirectURL is matching port on listener - c.Assert(s.URL+"/callback", qt.Equals, redirectURL) - - return s -} - // TestBrowserAuth goes through the flow of a browser logging in, simulating // the cookie state and handling the callbacks are as expected. Additionally handling // the final callback to the dashboard emulating an endpoint. See setupTestServer @@ -96,72 +40,31 @@ func TestBrowserAuth(t *testing.T) { c := qt.New(t) db, sessionStore := setupDbAndSessionStore(c) - - // Setup final test redirect url server, to emulate - // the dashboard receiving the final piece of the flow - dashboardResponse := "dashboard received final callback" - dashboard := httptest.NewServer( - http.HandlerFunc( - func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, dashboardResponse) - sessionCookie, _ := r.Cookie("jimm-browser-session") - c.Assert(sessionCookie.Name, qt.Equals, "jimm-browser-session") - c.Assert(sessionCookie.Value, qt.Not(qt.Equals), "") - // Check the session exist in db - session, err := sessionStore.Get(r, "jimm-browser-session") - c.Assert(err, qt.IsNil) - c.Assert(session.Values["jimm-session"], qt.Equals, "jimm-test@canonical.com") - }, - ), - ) - defer dashboard.Close() - - s := setupTestServer(c, dashboard.URL, db, sessionStore) - defer s.Close() - - jar, err := cookiejar.New(nil) + cookie, err := jimmtest.RunBrowserLogin(db, sessionStore, 60) c.Assert(err, qt.IsNil) - - client := &http.Client{ - Jar: jar, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - fmt.Println("redirected to", req.URL) - return nil - }, - } - - res, err := client.Get(s.URL + "/login") - c.Assert(err, qt.IsNil) - c.Assert(res.StatusCode, qt.Equals, http.StatusOK) - - defer res.Body.Close() - b, err := io.ReadAll(res.Body) - c.Assert(err, qt.IsNil) - - re := regexp.MustCompile(`action="(.*?)" method=`) - match := re.FindStringSubmatch(string(b)) - loginFormUrl := match[1] - - v := url.Values{} - v.Add("username", "jimm-test") - v.Add("password", "password") - loginResp, err := client.PostForm(loginFormUrl, v) - c.Assert(err, qt.IsNil) - - b, err = io.ReadAll(loginResp.Body) - c.Assert(err, qt.IsNil) - - c.Assert(string(b), qt.Equals, dashboardResponse) - c.Assert(loginResp.StatusCode, qt.Equals, 200) - - defer loginResp.Body.Close() + c.Assert(cookie, qt.Not(qt.Equals), "") + + // // Get the decrypted session by falseifying (cant spell) the request + // r, _ := http.NewRequest("", "", nil) + // r.Header.Set("Cookie", cookie) + // session, err := sessionStore.Get(r, auth.SessionName) + // c.Assert(err, qt.IsNil) + // fmt.Println(session) + + // // Get the raw cookies by falseifying a save and retrieving set-cookie header + // w := httptest.NewRecorder() + // session.Options.MaxAge = 30 + // session.Save(r, w) + // decryptedCookie := w.Header().Get("Set-Cookie") + // c.Assert(decryptedCookie, qt.Equals, "digsdig") } func TestCallbackFailsNoCodePresent(t *testing.T) { c := qt.New(t) db, sessionStore := setupDbAndSessionStore(c) - s := setupTestServer(c, "", db, sessionStore) + s, err := jimmtest.SetupTestDashboardCallbackHandler("", db, sessionStore, 60) + c.Assert(err, qt.IsNil) defer s.Close() // Test with no code present at all @@ -179,7 +82,8 @@ func TestCallbackFailsExchange(t *testing.T) { c := qt.New(t) db, sessionStore := setupDbAndSessionStore(c) - s := setupTestServer(c, "", db, sessionStore) + s, err := jimmtest.SetupTestDashboardCallbackHandler("", db, sessionStore, 60) + c.Assert(err, qt.IsNil) defer s.Close() // Test with no code present at all diff --git a/internal/jimmhttp/websocket.go b/internal/jimmhttp/websocket.go index 178e22494..caec08be5 100644 --- a/internal/jimmhttp/websocket.go +++ b/internal/jimmhttp/websocket.go @@ -12,6 +12,8 @@ import ( "github.com/juju/zaputil/zapctx" "go.uber.org/zap" + "github.com/canonical/jimm/internal/auth" + "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/servermon" ) @@ -33,6 +35,34 @@ type WSHandler struct { // been started. func (h *WSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctx := req.Context() + + // We perform cookie authentication at the HTTP layer instead of WS + // due to limitations of setting and retrieving cookies in the WS layer. + // + // If no cookie is present, we expect 1 of 3 scenarios: + // 1. It's a device session token login. + // 2. It's a client credential login. + // 3. It's an "expired" cookie login, and as such no cookie + // has been sent with the request. The handling of this is within + // LoginWithSessionCookie, in which, due to no identityId being present + // we know the cookie expired or a request with no cookie was made. + _, err := req.Cookie(auth.SessionName) + + // Now we know a cookie is present, so let's try perform a cookie login / logic + // as presumably a cookie of this name should only ever be present in the case + // the browser performs a connection. + if err == nil { + ctx, err = h.Server.GetAuthenticationService().AuthenticateBrowserSession( + ctx, w, req, + ) + if err != nil { + // Something went wrong when trying to perform the authentication + // of the cookie. + w.WriteHeader(http.StatusInternalServerError) + return + } + } + ctx = context.WithValue(ctx, contextPathKey("path"), req.URL.EscapedPath()) conn, err := h.Upgrader.Upgrade(w, req, nil) if err != nil { @@ -70,4 +100,7 @@ func (h *WSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { // the websocket connection, but not send any control messages. type WSServer interface { ServeWS(context.Context, *websocket.Conn) + + // GetAuthenticationService returns JIMM's authentication services. + GetAuthenticationService() jimm.OAuthAuthenticator } diff --git a/internal/jimmhttp/websocket_test.go b/internal/jimmhttp/websocket_test.go index 8542d2483..d509088c2 100644 --- a/internal/jimmhttp/websocket_test.go +++ b/internal/jimmhttp/websocket_test.go @@ -12,6 +12,7 @@ import ( qt "github.com/frankban/quicktest" "github.com/gorilla/websocket" + "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/jimmhttp" ) @@ -57,6 +58,11 @@ func (s echoServer) ServeWS(ctx context.Context, conn *websocket.Conn) { } } +// GetAuthenticationService returns JIMM's oauth authentication service. +func (s echoServer) GetAuthenticationService() jimm.OAuthAuthenticator { + return nil +} + func TestWSHandlerPanic(t *testing.T) { c := qt.New(t) @@ -77,6 +83,11 @@ func TestWSHandlerPanic(t *testing.T) { type panicServer struct{} +// GetAuthenticationService returns JIMM's oauth authentication service. +func (s panicServer) GetAuthenticationService() jimm.OAuthAuthenticator { + return nil +} + func (s panicServer) ServeWS(ctx context.Context, conn *websocket.Conn) { panic("test") } diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index 62dc52aa3..30a4e5f99 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -5,16 +5,31 @@ package jimmtest import ( "context" "encoding/base64" + "errors" + "fmt" + "io" + "math/rand" + "net" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "net/url" + "regexp" + "strconv" "strings" "time" + "github.com/coreos/go-oidc/v3/oidc" + "github.com/gorilla/sessions" "github.com/juju/juju/api" jujuparams "github.com/juju/juju/rpc/params" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/canonical/jimm/internal/auth" + "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/jimm" + "github.com/canonical/jimm/internal/jimmhttp" "github.com/canonical/jimm/internal/openfga" ) @@ -75,3 +90,144 @@ func convertUsernameToEmail(username string) string { } return username } + +func SetupTestDashboardCallbackHandler(browserURL string, db *db.Database, sessionStore sessions.Store, cookieExpiry int) (*httptest.Server, error) { + // Create unstarted server to enable auth service + s := httptest.NewUnstartedServer(nil) + // Setup random port listener + minPort := 30000 + maxPort := 50000 + + port := strconv.Itoa(rand.Intn(maxPort-minPort+1) + minPort) + l, err := net.Listen("tcp", "localhost:"+port) + if err != nil { + return nil, err + } + + // Set the listener with a random port + s.Listener = l + + // Remember redirect url to check it matches after test server starts + redirectURL := "http://127.0.0.1:" + port + "/callback" + authSvc, err := auth.NewAuthenticationService(context.Background(), auth.AuthenticationServiceParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Hour, + // Now we know the port the test server is running on + RedirectURL: redirectURL, + Store: db, + SessionStore: sessionStore, + SessionCookieMaxAge: 60, + }) + if err != nil { + return nil, err + } + + h, err := jimmhttp.NewOAuthHandler(jimmhttp.OAuthHandlerParams{ + Authenticator: authSvc, + DashboardFinalRedirectURL: browserURL, + SecureCookies: false, + CookieExpiry: cookieExpiry, + }) + if err != nil { + return nil, err + } + + s.Config.Handler = h.Routes() + + s.Start() + + // Ensure redirectURL is matching port on listener + if s.URL+"/callback" != redirectURL { + return s, errors.New("server callback does not match redirectURL") + } + + return s, nil +} + +func RunBrowserLogin(db *db.Database, sessionStore sessions.Store, cookieExpiry int) (string, error) { + var cookieString string + + // Setup final test redirect url server, to emulate + // the dashboard receiving the final piece of the flow + dashboardResponse := "dashboard received final callback" + browser := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + cookieString = r.Header.Get("Cookie") + w.Write([]byte(dashboardResponse)) + }, + ), + ) + defer browser.Close() + + s, err := SetupTestDashboardCallbackHandler(browser.URL, db, sessionStore, cookieExpiry) + if err != nil { + return cookieString, err + } + defer s.Close() + + jar, err := cookiejar.New(nil) + if err != nil { + return cookieString, err + } + + client := &http.Client{ + Jar: jar, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + fmt.Println("redirected to", req.URL) + return nil + }, + } + + res, err := client.Get(s.URL + "/login") + if err != nil { + return cookieString, err + } + + if res.StatusCode != http.StatusOK { + return cookieString, errors.New("status code not ok") + } + + defer res.Body.Close() + b, err := io.ReadAll(res.Body) + if err != nil { + return cookieString, err + } + + re := regexp.MustCompile(`action="(.*?)" method=`) + match := re.FindStringSubmatch(string(b)) + loginFormUrl := match[1] + + v := url.Values{} + v.Add("username", "jimm-test") + v.Add("password", "password") + loginResp, err := client.PostForm(loginFormUrl, v) + if err != nil { + return cookieString, err + } + + b, err = io.ReadAll(loginResp.Body) + if err != nil { + return cookieString, err + } + + if string(b) != dashboardResponse { + return cookieString, errors.New("dashboard response not equal") + } + if loginResp.StatusCode != http.StatusOK { + return cookieString, errors.New("status code not ok") + } + + loginResp.Body.Close() + return cookieString, nil +} + +func ParseCookies(cookies string) []*http.Cookie { + header := http.Header{} + header.Add("Cookie", cookies) + request := http.Request{Header: header} + return request.Cookies() +} diff --git a/internal/jimmtest/suite.go b/internal/jimmtest/suite.go index 01fc0d9a8..6df43706b 100644 --- a/internal/jimmtest/suite.go +++ b/internal/jimmtest/suite.go @@ -69,10 +69,12 @@ func (s *JIMMSuite) SetUpTest(c *gc.C) { s.OFGAClient, s.COFGAClient, s.COFGAParams, err = SetupTestOFGAClient(c.TestName()) c.Assert(err, gc.IsNil) + pgdb := PostgresDB(GocheckTester{c}, nil) + // Setup OpenFGA. s.JIMM = &jimm.JIMM{ Database: db.Database{ - DB: PostgresDB(GocheckTester{c}, nil), + DB: pgdb, }, CredentialStore: &InMemoryCredentialStore{}, Pubsub: &pubsub.Hub{MaxConcurrency: 10}, diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index 80868248b..6005db250 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -100,6 +100,40 @@ func (r *controllerRoot) GetDeviceSessionToken(ctx context.Context) (params.GetD return response, nil } +// LoginWithSessionCookie +func (r *controllerRoot) LoginWithSessionCookie(ctx context.Context) (jujuparams.LoginResult, error) { + const op = errors.Op("jujuapi.LoginWithSessionCookie") + + // If no identity ID has come through, then no cookie was present + // and as such authentication has failed. + if r.identityId == "" { + return jujuparams.LoginResult{}, errors.E(op, (&auth.AuthenticationError{}).Error()) + } + + user, err := r.jimm.GetOpenFGAUserAndAuthorise(ctx, r.identityId) + if err != nil { + return jujuparams.LoginResult{}, errors.E(op, err) + } + + r.mu.Lock() + r.user = user + r.mu.Unlock() + + // Get server version for LoginResult + srvVersion, err := r.jimm.EarliestControllerVersion(ctx) + if err != nil { + return jujuparams.LoginResult{}, errors.E(op, err) + } + + return jujuparams.LoginResult{ + PublicDNSName: r.params.PublicDNSName, + UserInfo: setupAuthUserInfo(ctx, r, user), + ControllerTag: setupControllerTag(r), + Facades: setupFacades(r), + ServerVersion: srvVersion.String(), + }, nil +} + // LoginWithSessionToken handles logging into the JIMM via a session token that JIMM has // minted itself, this session token is simply a JWT containing the users email // at which point the email is used to perform a lookup for the user, authorise diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index 4bce1d761..cf7f0e9da 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -4,9 +4,11 @@ package jujuapi_test import ( "context" + "crypto/tls" "encoding/base64" "fmt" "io" + "net" "net/http" "net/http/cookiejar" "net/url" @@ -14,14 +16,19 @@ import ( "strings" "time" + "github.com/antonlindstrom/pgstore" "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/jimmtest" + "github.com/gorilla/websocket" "github.com/coreos/go-oidc/v3/oidc" + "github.com/juju/errors" "github.com/juju/juju/api" + "github.com/juju/juju/rpc/jsoncodec" jujuparams "github.com/juju/juju/rpc/params" + "github.com/juju/juju/utils/proxy" "github.com/juju/names/v4" gc "gopkg.in/check.v1" ) @@ -34,15 +41,23 @@ func (s *adminSuite) SetUpTest(c *gc.C) { s.websocketSuite.SetUpTest(c) ctx := context.Background() + sqldb, err := s.JIMM.Database.DB.DB() + c.Assert(err, gc.IsNil) + + sessionStore, err := pgstore.NewPGStoreFromPool(sqldb, []byte("secretsecretdigletts")) + c.Assert(err, gc.IsNil) + // Replace JIMM's mock authenticator with a real one here // for testing the login flows. authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Hour, - Store: &s.JIMM.Database, + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Hour, + Store: &s.JIMM.Database, + SessionStore: sessionStore, + SessionCookieMaxAge: 60, }) c.Assert(err, gc.Equals, nil) s.JIMM.OAuthAuthenticator = authSvc @@ -62,6 +77,96 @@ func (s *adminSuite) TestLoginToController(c *gc.C) { c.Assert(jujuparams.ErrCode(err), gc.Equals, jujuparams.CodeNotImplemented) } +// TestBrowserLogin takes a test user through the flow of logging into jimm +// via the correct facades. All are done in a single test to see the flow end-2-end. +// +// Within the test are clear comments explaining what is happening when and why. +// Please refer to these comments for further details. +func (s *adminSuite) TestBrowserLogin(c *gc.C) { + // The setup runs a browser login with callback, ultimately retrieving + // a logged in user by cookie. + sqldb, err := s.JIMM.DB().DB.DB() + c.Assert(err, gc.IsNil) + + sessionStore, err := pgstore.NewPGStoreFromPool(sqldb, []byte("secretsecretdigletts")) + c.Assert(err, gc.IsNil) + + cookie, err := jimmtest.RunBrowserLogin(s.JIMM.DB(), sessionStore, 60) + c.Assert(err, gc.IsNil) + c.Assert(cookie, gc.Not(gc.Equals), "") + + cookies := jimmtest.ParseCookies(cookie) + c.Assert(cookies, gc.HasLen, 1) + + jar, err := cookiejar.New(nil) + c.Assert(err, gc.IsNil) + + // Now we move this cookie to the JIMM server on the admin suite and + // set the cookie on the jimm test server url so that the cookie can be + // sent on WS calls. + jimmURL, err := url.Parse(s.Server.URL) + c.Assert(err, gc.IsNil) + jar.SetCookies(jimmURL, cookies) + + // Copied from github.com/juju/juju@v0.0.0-20240304110523-55fb5d03683b/api/apiclient.go + dialWebsocket := func(ctx context.Context, urlStr string, tlsConfig *tls.Config, ipAddr string) (jsoncodec.JSONConn, error) { + url, err := url.Parse(urlStr) + if err != nil { + return nil, errors.Trace(err) + } + + netDialer := net.Dialer{} + dialer := &websocket.Dialer{ + NetDial: func(netw, addr string) (net.Conn, error) { + if addr == url.Host { + addr = ipAddr + } + return netDialer.DialContext(ctx, netw, addr) + }, + Proxy: proxy.DefaultConfig.GetProxy, + HandshakeTimeout: 45 * time.Second, + TLSClientConfig: tlsConfig, + Jar: jar, + } + + c, resp, err := dialer.Dial(urlStr, nil) + if err != nil { + if err == websocket.ErrBadHandshake { + defer resp.Body.Close() + body, readErr := io.ReadAll(resp.Body) + if readErr == nil { + err = errors.Errorf( + "%s (%s)", + strings.TrimSpace(string(body)), + http.StatusText(resp.StatusCode), + ) + } + } + return nil, errors.Trace(err) + } + return jsoncodec.NewWebsocketConn(c), nil + } + + conn := s.openWithDialWebsocket( + c, + &api.Info{ + SkipLogin: true, + }, + "test", + dialWebsocket, + ) + defer conn.Close() + + lr := &jujuparams.LoginResult{} + c.Assert( + conn.APICall("Admin", 4, "", "LoginWithSessionCookie", nil, lr), + gc.IsNil, + ) + + c.Assert(lr.UserInfo.Identity, gc.Equals, "user-jimm-test@canonical.com") + c.Assert(lr.UserInfo.DisplayName, gc.Equals, "jimm-test") +} + // TestDeviceLogin takes a test user through the flow of logging into jimm // via the correct facades. All are done in a single test to see the flow end-2-end. // diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index 883cd9881..39661bcf5 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -129,9 +129,12 @@ type controllerRoot struct { // is created per WS, it is EXPECTED that the subsequent call to GetDeviceSessionToken // happens on the SAME websocket. deviceOAuthResponse *oauth2.DeviceAuthResponse + + // identityId is the id of the identity attempting to login via a session cookie. + identityId string } -func newControllerRoot(j JIMM, p Params) *controllerRoot { +func newControllerRoot(j JIMM, p Params, identityId string) *controllerRoot { watcherRegistry := &watcherRegistry{ watchers: make(map[string]*modelSummaryWatcher), } @@ -141,6 +144,8 @@ func newControllerRoot(j JIMM, p Params) *controllerRoot { watchers: watcherRegistry, pingF: func() {}, controllerUUIDMasking: true, + user: nil, // TODO + identityId: identityId, } r.AddMethod("Admin", 1, "Login", rpc.Method(unsupportedLogin)) @@ -150,6 +155,7 @@ func newControllerRoot(j JIMM, p Params) *controllerRoot { r.AddMethod("Admin", 4, "LoginDevice", rpc.Method(r.LoginDevice)) r.AddMethod("Admin", 4, "GetDeviceSessionToken", rpc.Method(r.GetDeviceSessionToken)) r.AddMethod("Admin", 4, "LoginWithSessionToken", rpc.Method(r.LoginWithSessionToken)) + r.AddMethod("Admin", 4, "LoginWithSessionCookie", rpc.Method(r.LoginWithSessionCookie)) r.AddMethod("Admin", 4, "LoginWithClientCredentials", rpc.Method(r.LoginWithClientCredentials)) r.AddMethod("Pinger", 1, "Ping", rpc.Method(r.Ping)) return r diff --git a/internal/jujuapi/export_test.go b/internal/jujuapi/export_test.go index e5ac3a80c..754435102 100644 --- a/internal/jujuapi/export_test.go +++ b/internal/jujuapi/export_test.go @@ -46,7 +46,7 @@ func ToJAASTag(db db.Database, tag *ofganames.Tag) (string, error) { } func NewControllerRoot(j JIMM, p Params) *controllerRoot { - return newControllerRoot(j, p) + return newControllerRoot(j, p, "") } func (r *controllerRoot) GetServiceAccount(ctx context.Context, clientID string) (*openfga.User, error) { diff --git a/internal/jujuapi/pinger_internal_test.go b/internal/jujuapi/pinger_internal_test.go index 5495156dd..8fbe84ece 100644 --- a/internal/jujuapi/pinger_internal_test.go +++ b/internal/jujuapi/pinger_internal_test.go @@ -14,7 +14,7 @@ import ( func TestControllerPing(t *testing.T) { c := qt.New(t) - r := newControllerRoot(nil, Params{}) + r := newControllerRoot(nil, Params{}, "") defer r.cleanup() var calls uint32 r.setPingF(func() { atomic.AddUint32(&calls, 1) }) diff --git a/internal/jujuapi/websocket.go b/internal/jujuapi/websocket.go index 0d92260a5..4bc96b4a9 100644 --- a/internal/jujuapi/websocket.go +++ b/internal/jujuapi/websocket.go @@ -17,6 +17,7 @@ import ( "github.com/juju/zaputil/zapctx" "go.uber.org/zap" + "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/jimm" @@ -43,9 +44,15 @@ type apiServer struct { params Params } +// GetAuthenticationService returns JIMM's oauth authentication service. +func (s *apiServer) GetAuthenticationService() jimm.OAuthAuthenticator { + return s.jimm.OAuthAuthenticator +} + // ServeWS implements jimmhttp.WSServer. -func (s *apiServer) ServeWS(_ context.Context, conn *websocket.Conn) { - controllerRoot := newControllerRoot(s.jimm, s.params) +func (s *apiServer) ServeWS(ctx context.Context, conn *websocket.Conn) { + identityId := auth.SessionIdentityFromContext(ctx) + controllerRoot := newControllerRoot(s.jimm, s.params, identityId) s.cleanup = controllerRoot.cleanup Dblogger := controllerRoot.newAuditLogger() serveRoot(context.Background(), controllerRoot, Dblogger, conn) @@ -128,6 +135,11 @@ func modelInfoFromPath(path string) (uuid string, finalPath string, err error) { return matches[modelIndex], matches[finalPathIndex], nil } +// GetAuthenticationService returns JIMM's oauth authentication service. +func (s modelProxyServer) GetAuthenticationService() jimm.OAuthAuthenticator { + return s.jimm.OAuthAuthenticator +} + // ServeWS implements jimmhttp.WSServer. func (s modelProxyServer) ServeWS(ctx context.Context, clientConn *websocket.Conn) { // TODO(CSS-7331) Refactor model proxy for new login methods diff --git a/internal/jujuapi/websocket_test.go b/internal/jujuapi/websocket_test.go index 087accfc1..a1f17247b 100644 --- a/internal/jujuapi/websocket_test.go +++ b/internal/jujuapi/websocket_test.go @@ -5,6 +5,7 @@ package jujuapi_test import ( "bytes" "context" + "crypto/tls" "encoding/pem" "fmt" "net/http" @@ -12,6 +13,7 @@ import ( "net/url" "github.com/juju/juju/api" + "github.com/juju/juju/rpc/jsoncodec" jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" gc "gopkg.in/check.v1" @@ -99,7 +101,12 @@ func (s *websocketSuite) TearDownTest(c *gc.C) { // openNoAssert creates a new websocket connection to the test server, using the // connection info specified in info, authenticating as the given user. // If info is nil then default values will be used. -func (s *websocketSuite) openNoAssert(c *gc.C, info *api.Info, username string) (api.Connection, error) { +func (s *websocketSuite) openNoAssert( + c *gc.C, + info *api.Info, + username string, + dialWebsocket func(ctx context.Context, urlStr string, tlsConfig *tls.Config, ipAddr string) (jsoncodec.JSONConn, error), +) (api.Connection, error) { var inf api.Info if info != nil { inf = *info @@ -119,14 +126,31 @@ func (s *websocketSuite) openNoAssert(c *gc.C, info *api.Info, username string) lp := jimmtest.NewUserSessionLogin(username) - return api.Open(&inf, api.DialOpts{ + dialOpts := api.DialOpts{ InsecureSkipVerify: true, LoginProvider: lp, - }) + } + + if dialWebsocket != nil { + dialOpts.DialWebsocket = dialWebsocket + } + + return api.Open(&inf, dialOpts) } func (s *websocketSuite) open(c *gc.C, info *api.Info, username string) api.Connection { - conn, err := s.openNoAssert(c, info, username) + conn, err := s.openNoAssert(c, info, username, nil) + c.Assert(err, gc.Equals, nil) + return conn +} + +func (s *websocketSuite) openWithDialWebsocket( + c *gc.C, + info *api.Info, + username string, + dialWebsocket func(ctx context.Context, urlStr string, tlsConfig *tls.Config, ipAddr string) (jsoncodec.JSONConn, error), +) api.Connection { + conn, err := s.openNoAssert(c, info, username, dialWebsocket) c.Assert(err, gc.Equals, nil) return conn } diff --git a/service.go b/service.go index 3d152c5fd..38c4de6cc 100644 --- a/service.go +++ b/service.go @@ -263,10 +263,8 @@ func NewService(ctx context.Context, p Params) (*Service, error) { if err != nil { return nil, errors.E(op, err) } - // Cleanup expired session every 30 minutes defer sessionStore.StopCleanup(sessionStore.Cleanup(time.Minute * 30)) - s.jimm.CookieSessionStore = sessionStore if p.AuditLogRetentionPeriodInDays != "" { period, err := strconv.Atoi(p.AuditLogRetentionPeriodInDays) @@ -299,6 +297,7 @@ func NewService(ctx context.Context, p Params) (*Service, error) { Scopes: p.OAuthAuthenticatorParams.Scopes, SessionTokenExpiry: p.OAuthAuthenticatorParams.SessionTokenExpiry, Store: &s.jimm.Database, + SessionStore: sessionStore, }, ) s.jimm.OAuthAuthenticator = authSvc @@ -353,7 +352,6 @@ func NewService(ctx context.Context, p Params) (*Service, error) { oauthHandler, err := jimmhttp.NewOAuthHandler(jimmhttp.OAuthHandlerParams{ Authenticator: authSvc, DashboardFinalRedirectURL: p.DashboardFinalRedirectURL, - SessionStore: sessionStore, SecureCookies: p.SecureSessionCookies, CookieExpiry: p.SessionCookieExpiry, }) From 12c20db651e0a8f2c6009494f7455d785bae0d80 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Mon, 18 Mar 2024 11:37:42 +0200 Subject: [PATCH 076/126] RIP Candid - Follow up (#1174) * Improve test setup * Remove unneeded TODO --- cmd/jaas/cmd/addserviceaccount_test.go | 4 ++-- cmd/jaas/cmd/grant_test.go | 4 ++-- .../cmd/listserviceaccountcredentials_test.go | 2 +- cmd/jaas/cmd/updatecredentials_test.go | 10 +++++----- cmd/jimmctl/cmd/addcloudtocontroller_test.go | 2 +- cmd/jimmctl/cmd/addcontroller_test.go | 4 ++-- cmd/jimmctl/cmd/crossmodelquery_test.go | 2 +- cmd/jimmctl/cmd/grantauditlogaccess_test.go | 4 ++-- cmd/jimmctl/cmd/group_test.go | 18 ++++++++--------- .../cmd/importcloudcredentials_test.go | 2 +- cmd/jimmctl/cmd/importmodel_test.go | 14 ++++++------- cmd/jimmctl/cmd/listauditevents_test.go | 4 ++-- cmd/jimmctl/cmd/listcontrollers_test.go | 4 ++-- cmd/jimmctl/cmd/migratemodel_test.go | 6 +++--- cmd/jimmctl/cmd/modelstatus_test.go | 4 ++-- cmd/jimmctl/cmd/purge_logs_test.go | 8 ++++---- cmd/jimmctl/cmd/relation_test.go | 20 +++++++++---------- .../cmd/removecloudfromcontroller_test.go | 6 +++--- cmd/jimmctl/cmd/removecontroller_test.go | 4 ++-- cmd/jimmctl/cmd/revokeauditlogaccess_test.go | 4 ++-- .../cmd/setcontrollerdeprecated_test.go | 4 ++-- cmd/jimmctl/cmd/updatemigratedmodel_test.go | 12 +++++------ internal/jimm/access_test.go | 4 ++-- internal/jimmtest/auth.go | 17 +++++++++++++--- internal/jujuapi/websocket_test.go | 2 +- service.go | 1 - service_test.go | 8 ++++---- 27 files changed, 92 insertions(+), 82 deletions(-) diff --git a/cmd/jaas/cmd/addserviceaccount_test.go b/cmd/jaas/cmd/addserviceaccount_test.go index 32d9aa448..18ed7f18e 100644 --- a/cmd/jaas/cmd/addserviceaccount_test.go +++ b/cmd/jaas/cmd/addserviceaccount_test.go @@ -26,7 +26,7 @@ var _ = gc.Suite(&addServiceAccountSuite{}) func (s *addServiceAccountSuite) TestAddServiceAccount(c *gc.C) { clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err := cmdtesting.RunCommand(c, cmd.NewAddServiceAccountCommandForTesting(s.ClientStore(), bClient), clientID) c.Assert(err, gc.IsNil) tuple := openfga.Tuple{ @@ -42,7 +42,7 @@ func (s *addServiceAccountSuite) TestAddServiceAccount(c *gc.C) { _, err = cmdtesting.RunCommand(c, cmd.NewAddServiceAccountCommandForTesting(s.ClientStore(), bClient), clientID) c.Assert(err, gc.IsNil) // Check that re-running the command for a different user returns an error. - bClientBob := jimmtest.NewUserSessionLogin("bob") + bClientBob := jimmtest.NewUserSessionLogin(c, "bob") _, err = cmdtesting.RunCommand(c, cmd.NewAddServiceAccountCommandForTesting(s.ClientStore(), bClientBob), clientID) c.Assert(err, gc.ErrorMatches, "service account already owned") } diff --git a/cmd/jaas/cmd/grant_test.go b/cmd/jaas/cmd/grant_test.go index 416789aa7..042d167cb 100644 --- a/cmd/jaas/cmd/grant_test.go +++ b/cmd/jaas/cmd/grant_test.go @@ -30,7 +30,7 @@ func (s *grantSuite) TestGrant(c *gc.C) { clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") sa := dbmodel.Identity{ Name: clientID, @@ -86,7 +86,7 @@ func (s *grantSuite) TestMissingArgs(c *gc.C) { expectedError: "user/group not specified", }} - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") clientStore := s.ClientStore() for _, t := range tests { _, err := cmdtesting.RunCommand(c, cmd.NewGrantCommandForTesting(clientStore, bClient), t.args...) diff --git a/cmd/jaas/cmd/listserviceaccountcredentials_test.go b/cmd/jaas/cmd/listserviceaccountcredentials_test.go index 7769297bc..03bf8184c 100644 --- a/cmd/jaas/cmd/listserviceaccountcredentials_test.go +++ b/cmd/jaas/cmd/listserviceaccountcredentials_test.go @@ -99,7 +99,7 @@ aws foo } for _, test := range testCases { c.Log(test.about) - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") var result *jujucmd.Context if test.showSecrets { result, err = cmdtesting.RunCommand(c, cmd.NewListServiceAccountCredentialsCommandForTesting(s.ClientStore(), bClient), clientID, "--format", test.format, "--show-secrets") diff --git a/cmd/jaas/cmd/updatecredentials_test.go b/cmd/jaas/cmd/updatecredentials_test.go index e803de39e..e57a453ec 100644 --- a/cmd/jaas/cmd/updatecredentials_test.go +++ b/cmd/jaas/cmd/updatecredentials_test.go @@ -31,7 +31,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithNewCredentials(c *gc.C clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") sa := dbmodel.Identity{ Name: clientID, @@ -92,7 +92,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") sa := dbmodel.Identity{ Name: clientID, @@ -157,7 +157,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c } func (s *updateCredentialsSuite) TestCloudNotInLocalStore(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(s.ClientStore(), bClient), "00000000-0000-0000-0000-000000000000", "non-existing-cloud", @@ -167,7 +167,7 @@ func (s *updateCredentialsSuite) TestCloudNotInLocalStore(c *gc.C) { } func (s *updateCredentialsSuite) TestCredentialNotInLocalStore(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") clientStore := s.ClientStore() err := clientStore.UpdateCredential("some-cloud", jujucloud.CloudCredential{ @@ -208,7 +208,7 @@ func (s *updateCredentialsSuite) TestMissingArgs(c *gc.C) { expectedError: "too many args", }} - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") clientStore := s.ClientStore() for _, t := range tests { _, err := cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(clientStore, bClient), t.args...) diff --git a/cmd/jimmctl/cmd/addcloudtocontroller_test.go b/cmd/jimmctl/cmd/addcloudtocontroller_test.go index 3c4052ed9..8e6e38374 100644 --- a/cmd/jimmctl/cmd/addcloudtocontroller_test.go +++ b/cmd/jimmctl/cmd/addcloudtocontroller_test.go @@ -168,7 +168,7 @@ clouds: c.Log(test.about) tmpfile, cleanupFunc := writeTempFile(c, test.cloudInfo) - bClient := jimmtest.NewUserSessionLogin("bob@canonical.com") + bClient := jimmtest.NewUserSessionLogin(c, "bob@canonical.com") // Running the command succeeds newCmd := cmd.NewAddCloudToControllerCommandForTesting(s.ClientStore(), bClient, test.cloudByNameFunc) var err error diff --git a/cmd/jimmctl/cmd/addcontroller_test.go b/cmd/jimmctl/cmd/addcontroller_test.go index aee99a0e1..3dca0047f 100644 --- a/cmd/jimmctl/cmd/addcontroller_test.go +++ b/cmd/jimmctl/cmd/addcontroller_test.go @@ -38,7 +38,7 @@ func (s *addControllerSuite) TestAddControllerSuperuser(c *gc.C) { defer os.RemoveAll(tmpdir) // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") ctx, err := cmdtesting.RunCommand(c, cmd.NewAddControllerCommandForTesting(s.ClientStore(), bClient), tmpfile) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(ctx), gc.Matches, `name: controller-1 @@ -101,7 +101,7 @@ func (s *addControllerSuite) TestAddController(c *gc.C) { defer os.RemoveAll(tmpdir) // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewAddControllerCommandForTesting(s.ClientStore(), bClient), tmpfile) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/crossmodelquery_test.go b/cmd/jimmctl/cmd/crossmodelquery_test.go index 50cfec5c8..9bcc46c3e 100644 --- a/cmd/jimmctl/cmd/crossmodelquery_test.go +++ b/cmd/jimmctl/cmd/crossmodelquery_test.go @@ -24,7 +24,7 @@ var _ = gc.Suite(&crossModelQuerySuite{}) func (s *crossModelQuerySuite) TestCrossModelQueryCommand(c *gc.C) { // Test setup. store := s.ClientStore() - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") s.AddController(c, "controller-2", s.APIInfo(c)) cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/alice@canonical.com/cred") diff --git a/cmd/jimmctl/cmd/grantauditlogaccess_test.go b/cmd/jimmctl/cmd/grantauditlogaccess_test.go index bdf32dd0d..4ca1c2d0c 100644 --- a/cmd/jimmctl/cmd/grantauditlogaccess_test.go +++ b/cmd/jimmctl/cmd/grantauditlogaccess_test.go @@ -20,14 +20,14 @@ type grantAuditLogAccessSuite struct { func (s *grantAuditLogAccessSuite) TestGrantAuditLogAccessSuperuser(c *gc.C) { // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err := cmdtesting.RunCommand(c, cmd.NewGrantAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@canonical.com") c.Assert(err, gc.IsNil) } func (s *grantAuditLogAccessSuite) TestGrantAuditLogAccess(c *gc.C) { // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewGrantAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@canonical.com") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/group_test.go b/cmd/jimmctl/cmd/group_test.go index 09f1cf306..235f0cb2c 100644 --- a/cmd/jimmctl/cmd/group_test.go +++ b/cmd/jimmctl/cmd/group_test.go @@ -24,7 +24,7 @@ var _ = gc.Suite(&groupSuite{}) func (s *groupSuite) TestAddGroupSuperuser(c *gc.C) { // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err := cmdtesting.RunCommand(c, cmd.NewAddGroupCommandForTesting(s.ClientStore(), bClient), "test-group") c.Assert(err, gc.IsNil) @@ -37,14 +37,14 @@ func (s *groupSuite) TestAddGroupSuperuser(c *gc.C) { func (s *groupSuite) TestAddGroup(c *gc.C) { // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewAddGroupCommandForTesting(s.ClientStore(), bClient), "test-group") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *groupSuite) TestRenameGroupSuperuser(c *gc.C) { // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") err := s.JimmCmdSuite.JIMM.Database.AddGroup(context.TODO(), "test-group") c.Assert(err, gc.IsNil) @@ -61,14 +61,14 @@ func (s *groupSuite) TestRenameGroupSuperuser(c *gc.C) { func (s *groupSuite) TestRenameGroup(c *gc.C) { // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewRenameGroupCommandForTesting(s.ClientStore(), bClient), "test-group", "renamed-group") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *groupSuite) TestRemoveGroupSuperuser(c *gc.C) { // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") err := s.JimmCmdSuite.JIMM.Database.AddGroup(context.TODO(), "test-group") c.Assert(err, gc.IsNil) @@ -83,7 +83,7 @@ func (s *groupSuite) TestRemoveGroupSuperuser(c *gc.C) { func (s *groupSuite) TestRemoveGroupWithoutFlag(c *gc.C) { // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err := cmdtesting.RunCommand(c, cmd.NewRemoveGroupCommandForTesting(s.ClientStore(), bClient), "test-group") c.Assert(err.Error(), gc.Matches, "Failed to read from input.") @@ -91,14 +91,14 @@ func (s *groupSuite) TestRemoveGroupWithoutFlag(c *gc.C) { func (s *groupSuite) TestRemoveGroup(c *gc.C) { // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewRemoveGroupCommandForTesting(s.ClientStore(), bClient), "test-group", "-y") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *groupSuite) TestListGroupsSuperuser(c *gc.C) { // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") for i := 0; i < 3; i++ { err := s.JimmCmdSuite.JIMM.Database.AddGroup(context.TODO(), fmt.Sprint("test-group", i)) @@ -115,7 +115,7 @@ func (s *groupSuite) TestListGroupsSuperuser(c *gc.C) { func (s *groupSuite) TestListGroups(c *gc.C) { // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewListGroupsCommandForTesting(s.ClientStore(), bClient), "test-group") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/importcloudcredentials_test.go b/cmd/jimmctl/cmd/importcloudcredentials_test.go index b7c9cd6c8..a77cc380b 100644 --- a/cmd/jimmctl/cmd/importcloudcredentials_test.go +++ b/cmd/jimmctl/cmd/importcloudcredentials_test.go @@ -64,7 +64,7 @@ func (s *importCloudCredentialsSuite) TestImportCloudCredentials(c *gc.C) { c.Assert(err, gc.IsNil) // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err = cmdtesting.RunCommand(c, cmd.NewImportCloudCredentialsCommandForTesting(s.ClientStore(), bClient), tmpfile) c.Assert(err, gc.IsNil) diff --git a/cmd/jimmctl/cmd/importmodel_test.go b/cmd/jimmctl/cmd/importmodel_test.go index fc221ef55..a4c7e12cc 100644 --- a/cmd/jimmctl/cmd/importmodel_test.go +++ b/cmd/jimmctl/cmd/importmodel_test.go @@ -43,7 +43,7 @@ func (s *importModelSuite) TestImportModelSuperuser(c *gc.C) { defer m.Close() // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err = cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-1", m.ModelUUID()) c.Assert(err, gc.IsNil) @@ -70,7 +70,7 @@ func (s *importModelSuite) TestImportModelFromLocalUser(c *gc.C) { c.Assert(err, gc.Equals, nil) // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err = cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-1", mt.Id(), "--owner", "alice@canonical.com") c.Assert(err, gc.IsNil) @@ -101,31 +101,31 @@ func (s *importModelSuite) TestImportModelUnauthorized(c *gc.C) { defer m.Close() // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err = cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-1", m.ModelUUID()) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *importModelSuite) TestImportModelNoController(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.ErrorMatches, `controller not specified`) } func (s *importModelSuite) TestImportModelNoModelUUID(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-id") c.Assert(err, gc.ErrorMatches, `model uuid not specified`) } func (s *importModelSuite) TestImportModelInvalidModelUUID(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-id", "not-a-uuid") c.Assert(err, gc.ErrorMatches, `invalid model uuid`) } func (s *importModelSuite) TestImportModelTooManyArgs(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewImportModelCommandForTesting(s.ClientStore(), bClient), "controller-id", "not-a-uuid", "spare-argument") c.Assert(err, gc.ErrorMatches, `too many args`) } diff --git a/cmd/jimmctl/cmd/listauditevents_test.go b/cmd/jimmctl/cmd/listauditevents_test.go index d66a8d8aa..0d8546fd6 100644 --- a/cmd/jimmctl/cmd/listauditevents_test.go +++ b/cmd/jimmctl/cmd/listauditevents_test.go @@ -27,7 +27,7 @@ func (s *listAuditEventsSuite) TestListAuditEventsSuperuser(c *gc.C) { s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") context, err := cmdtesting.RunCommand(c, cmd.NewListAuditEventsCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, @@ -66,7 +66,7 @@ func (s *listAuditEventsSuite) TestListAuditEventsStatus(c *gc.C) { s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewListAuditEventsCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/listcontrollers_test.go b/cmd/jimmctl/cmd/listcontrollers_test.go index e22cb9693..e23f3c331 100644 --- a/cmd/jimmctl/cmd/listcontrollers_test.go +++ b/cmd/jimmctl/cmd/listcontrollers_test.go @@ -77,7 +77,7 @@ func (s *listControllersSuite) TestListControllersSuperuser(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") context, err := cmdtesting.RunCommand(c, cmd.NewListControllersCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, expectedSuperuserOutput) @@ -87,7 +87,7 @@ func (s *listControllersSuite) TestListControllers(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") context, err := cmdtesting.RunCommand(c, cmd.NewListControllersCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, expectedOutput) diff --git a/cmd/jimmctl/cmd/migratemodel_test.go b/cmd/jimmctl/cmd/migratemodel_test.go index ac7bbec2e..3fc953a46 100644 --- a/cmd/jimmctl/cmd/migratemodel_test.go +++ b/cmd/jimmctl/cmd/migratemodel_test.go @@ -47,7 +47,7 @@ func (s *migrateModelSuite) TestMigrateModelCommandSuperuser(c *gc.C) { mt2 := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") context, err := cmdtesting.RunCommand(c, cmd.NewMigrateModelCommandForTesting(s.ClientStore(), bClient), "controller-1", mt.String(), mt2.String()) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, migrationResultRegex) @@ -61,13 +61,13 @@ func (s *migrateModelSuite) TestMigrateModelCommandFailsWithInvalidModelTag(c *g s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err := cmdtesting.RunCommand(c, cmd.NewMigrateModelCommandForTesting(s.ClientStore(), bClient), "controller-1", "model-001", "model-002") c.Assert(err, gc.ErrorMatches, ".* is not a valid model tag") } func (s *migrateModelSuite) TestMigrateModelCommandFailsWithMissingArgs(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err := cmdtesting.RunCommand(c, cmd.NewMigrateModelCommandForTesting(s.ClientStore(), bClient), "myController") c.Assert(err, gc.ErrorMatches, "Missing controller and model tag arguments") } diff --git a/cmd/jimmctl/cmd/modelstatus_test.go b/cmd/jimmctl/cmd/modelstatus_test.go index fbd85f577..150aa6bd4 100644 --- a/cmd/jimmctl/cmd/modelstatus_test.go +++ b/cmd/jimmctl/cmd/modelstatus_test.go @@ -61,7 +61,7 @@ func (s *modelStatusSuite) TestModelStatusSuperuser(c *gc.C) { mt := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") context, err := cmdtesting.RunCommand(c, cmd.NewModelStatusCommandForTesting(s.ClientStore(), bClient), mt.Id()) c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, expectedModelStatusOutput) @@ -75,7 +75,7 @@ func (s *modelStatusSuite) TestModelStatus(c *gc.C) { mt := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewModelStatusCommandForTesting(s.ClientStore(), bClient), mt.Id()) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/purge_logs_test.go b/cmd/jimmctl/cmd/purge_logs_test.go index f59f1f06b..d01193e9c 100644 --- a/cmd/jimmctl/cmd/purge_logs_test.go +++ b/cmd/jimmctl/cmd/purge_logs_test.go @@ -23,7 +23,7 @@ var _ = gc.Suite(&purgeLogsSuite{}) func (s *purgeLogsSuite) TestPurgeLogsSuperuser(c *gc.C) { // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") datastring := "2021-01-01T00:00:00Z" cmdCtx, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), datastring) c.Assert(err, gc.IsNil) @@ -34,7 +34,7 @@ func (s *purgeLogsSuite) TestPurgeLogsSuperuser(c *gc.C) { func (s *purgeLogsSuite) TestInvalidISO8601Date(c *gc.C) { // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") datastring := "13/01/2021" _, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), datastring) c.Assert(err, gc.ErrorMatches, `invalid date. Expected ISO8601 date`) @@ -43,7 +43,7 @@ func (s *purgeLogsSuite) TestInvalidISO8601Date(c *gc.C) { func (s *purgeLogsSuite) TestPurgeLogs(c *gc.C) { // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), "2021-01-01T00:00:00Z") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } @@ -85,7 +85,7 @@ func (s *purgeLogsSuite) TestPurgeLogsFromDb(c *gc.C) { tomorrow := relativeNow.AddDate(0, 0, 1).Format(layout) //alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") cmdCtx, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), tomorrow) c.Assert(err, gc.IsNil) // check that logs have been deleted diff --git a/cmd/jimmctl/cmd/relation_test.go b/cmd/jimmctl/cmd/relation_test.go index 1e8e48d22..4c9bcd752 100644 --- a/cmd/jimmctl/cmd/relation_test.go +++ b/cmd/jimmctl/cmd/relation_test.go @@ -39,7 +39,7 @@ var _ = gc.Suite(&relationSuite{}) func (s *relationSuite) TestAddRelationSuperuser(c *gc.C) { // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") group1 := "testGroup1" group2 := "testGroup2" type tuple struct { @@ -110,7 +110,7 @@ func (s *relationSuite) TestAddRelationSuperuser(c *gc.C) { func (s *relationSuite) TestMissingParamsAddRelationSuperuser(c *gc.C) { // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err := cmdtesting.RunCommand(c, cmd.NewAddRelationCommandForTesting(s.ClientStore(), bClient), "foo", "bar") c.Assert(err, gc.ErrorMatches, "target object not specified") @@ -123,7 +123,7 @@ func (s *relationSuite) TestMissingParamsAddRelationSuperuser(c *gc.C) { func (s *relationSuite) TestAddRelationViaFileSuperuser(c *gc.C) { // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") group1 := "testGroup1" group2 := "testGroup2" group3 := "testGroup3" @@ -152,14 +152,14 @@ func (s *relationSuite) TestAddRelationViaFileSuperuser(c *gc.C) { } func (s *relationSuite) TestAddRelationRejectsUnauthorisedUsers(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewAddRelationCommandForTesting(s.ClientStore(), bClient), "test-group1", "member", "test-group2") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *relationSuite) TestRemoveRelationSuperuser(c *gc.C) { // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") group1 := "testGroup1" group2 := "testGroup2" type tuple struct { @@ -205,7 +205,7 @@ func (s *relationSuite) TestRemoveRelationSuperuser(c *gc.C) { } func (s *relationSuite) TestRemoveRelationViaFileSuperuser(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") group1 := "testGroup1" group2 := "testGroup2" group3 := "testGroup3" @@ -251,7 +251,7 @@ func (s *relationSuite) TestRemoveRelationViaFileSuperuser(c *gc.C) { func (s *relationSuite) TestRemoveRelation(c *gc.C) { // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewRemoveRelationCommandForTesting(s.ClientStore(), bClient), "test-group1#member", "member", "test-group2") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } @@ -346,7 +346,7 @@ func initializeEnvironment(c *gc.C, ctx context.Context, db *db.Database, u dbmo func (s *relationSuite) TestListRelations(c *gc.C) { env := initializeEnvironment(c, context.Background(), &s.JIMM.Database, *s.AdminUser) // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") groups := []string{"group-1", "group-2", "group-3"} for _, group := range groups { @@ -439,7 +439,7 @@ user-eve@canonical.com administrator applicationoffer-test-controller-1:alice@ // TODO: remove boilerplate of env setup and use initialiseEnvironment func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { ctx := context.TODO() - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") ofgaClient := s.JIMM.OpenFGAClient // Add some resources to check against @@ -614,7 +614,7 @@ func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { func (s *relationSuite) TestCheckRelation(c *gc.C) { // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand( c, cmd.NewCheckRelationCommandForTesting(s.ClientStore(), bClient), diff --git a/cmd/jimmctl/cmd/removecloudfromcontroller_test.go b/cmd/jimmctl/cmd/removecloudfromcontroller_test.go index 038de08c5..77b5eb044 100644 --- a/cmd/jimmctl/cmd/removecloudfromcontroller_test.go +++ b/cmd/jimmctl/cmd/removecloudfromcontroller_test.go @@ -27,7 +27,7 @@ func (s *removeCloudFromControllerSuite) SetUpTest(c *gc.C) { } func (s *removeCloudFromControllerSuite) TestRemoveCloudFromController(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("alice@canonical.com") + bClient := jimmtest.NewUserSessionLogin(c, "alice@canonical.com") command := cmd.NewRemoveCloudFromControllerCommandForTesting( s.ClientStore(), @@ -49,7 +49,7 @@ func (s *removeCloudFromControllerSuite) TestRemoveCloudFromController(c *gc.C) } func (s *removeCloudFromControllerSuite) TestRemoveCloudFromControllerWrongArguments(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("alice@canonical.com") + bClient := jimmtest.NewUserSessionLogin(c, "alice@canonical.com") command := cmd.NewRemoveCloudFromControllerCommandForTesting( s.ClientStore(), @@ -64,7 +64,7 @@ func (s *removeCloudFromControllerSuite) TestRemoveCloudFromControllerWrongArgum } func (s *removeCloudFromControllerSuite) TestRemoveCloudFromControllerCloudNotFound(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("alice@canonical.com") + bClient := jimmtest.NewUserSessionLogin(c, "alice@canonical.com") command := cmd.NewRemoveCloudFromControllerCommandForTesting( s.ClientStore(), diff --git a/cmd/jimmctl/cmd/removecontroller_test.go b/cmd/jimmctl/cmd/removecontroller_test.go index be1226a6f..41bf48017 100644 --- a/cmd/jimmctl/cmd/removecontroller_test.go +++ b/cmd/jimmctl/cmd/removecontroller_test.go @@ -21,7 +21,7 @@ func (s *removeControllerSuite) TestRemoveControllerSuperuser(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") context, err := cmdtesting.RunCommand(c, cmd.NewRemoveControllerCommandForTesting(s.ClientStore(), bClient), "controller-1", "--force") c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, `name: controller-1 @@ -70,7 +70,7 @@ func (s *removeControllerSuite) TestRemoveController(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewRemoveControllerCommandForTesting(s.ClientStore(), bClient), "controller-1", "--force") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/revokeauditlogaccess_test.go b/cmd/jimmctl/cmd/revokeauditlogaccess_test.go index ccc5ad17b..860bf7c63 100644 --- a/cmd/jimmctl/cmd/revokeauditlogaccess_test.go +++ b/cmd/jimmctl/cmd/revokeauditlogaccess_test.go @@ -20,14 +20,14 @@ type revokeAuditLogAccessSuite struct { func (s *revokeAuditLogAccessSuite) TestRevokeAuditLogAccessSuperuser(c *gc.C) { // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err := cmdtesting.RunCommand(c, cmd.NewRevokeAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@canonical.com") c.Assert(err, gc.IsNil) } func (s *revokeAuditLogAccessSuite) TestRevokeAuditLogAccess(c *gc.C) { // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewRevokeAuditLogAccessCommandForTesting(s.ClientStore(), bClient), "bob@canonical.com") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go b/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go index 4d70ce69e..9126e1003 100644 --- a/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go +++ b/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go @@ -21,7 +21,7 @@ func (s *setControllerDeprecatedSuite) TestSetControllerDeprecatedSuperuser(c *g s.AddController(c, "controller-1", s.APIInfo(c)) // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") context, err := cmdtesting.RunCommand(c, cmd.NewSetControllerDeprecatedCommandForTesting(s.ClientStore(), bClient), "controller-1") c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(context), gc.Matches, `name: controller-1 @@ -70,7 +70,7 @@ func (s *setControllerDeprecatedSuite) TestSetControllerDeprecated(c *gc.C) { s.AddController(c, "controller-1", s.APIInfo(c)) // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewSetControllerDeprecatedCommandForTesting(s.ClientStore(), bClient), "controller-1") c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } diff --git a/cmd/jimmctl/cmd/updatemigratedmodel_test.go b/cmd/jimmctl/cmd/updatemigratedmodel_test.go index 7a6fe4ebe..987f07994 100644 --- a/cmd/jimmctl/cmd/updatemigratedmodel_test.go +++ b/cmd/jimmctl/cmd/updatemigratedmodel_test.go @@ -35,7 +35,7 @@ func (s *updateMigratedModelSuite) TestUpdateMigratedModelSuperuser(c *gc.C) { s.AddController(c, "controller-2", s.APIInfo(c)) // alice is superuser - bClient := jimmtest.NewUserSessionLogin("alice") + bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err = cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-2", mt.Id()) c.Assert(err, gc.IsNil) @@ -55,31 +55,31 @@ func (s *updateMigratedModelSuite) TestUpdateMigratedModelUnauthorized(c *gc.C) mt := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-2", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, cct) // bob is not superuser - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-1", mt.Id()) c.Assert(err, gc.ErrorMatches, `unauthorized \(unauthorized access\)`) } func (s *updateMigratedModelSuite) TestUpdateMigratedModelNoController(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient)) c.Assert(err, gc.ErrorMatches, `controller not specified`) } func (s *updateMigratedModelSuite) TestUpdateMigratedModelNoModelUUID(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-id") c.Assert(err, gc.ErrorMatches, `model uuid not specified`) } func (s *updateMigratedModelSuite) TestUpdateMigratedModelInvalidModelUUID(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-id", "not-a-uuid") c.Assert(err, gc.ErrorMatches, `invalid model uuid`) } func (s *updateMigratedModelSuite) TestUpdateMigratedModelTooManyArgs(c *gc.C) { - bClient := jimmtest.NewUserSessionLogin("bob") + bClient := jimmtest.NewUserSessionLogin(c, "bob") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateMigratedModelCommandForTesting(s.ClientStore(), bClient), "controller-id", "not-a-uuid", "spare-argument") c.Assert(err, gc.ErrorMatches, `too many args`) } diff --git a/internal/jimm/access_test.go b/internal/jimm/access_test.go index 42ebee6a5..f595f40b5 100644 --- a/internal/jimm/access_test.go +++ b/internal/jimm/access_test.go @@ -669,9 +669,9 @@ func TestResolveTagObjectMapsUsers(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - tag, err := jimm.ResolveTag(j.UUID, &j.Database, "user-alex@canonical.comly-werly#member") + tag, err := jimm.ResolveTag(j.UUID, &j.Database, "user-alex@canonical.com-werly#member") c.Assert(err, qt.IsNil) - c.Assert(tag, qt.DeepEquals, ofganames.ConvertTagWithRelation(names.NewUserTag("alex@canonical.comly-werly"), ofganames.MemberRelation)) + c.Assert(tag, qt.DeepEquals, ofganames.ConvertTagWithRelation(names.NewUserTag("alex@canonical.com-werly"), ofganames.MemberRelation)) } func TestResolveTupleObjectHandlesErrors(t *testing.T) { diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index 62dc52aa3..fe7066463 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -22,6 +22,14 @@ var ( jwtTestSecret = "test-secret" ) +// A SimpleTester is a simple version of the test interface +// that both the GoChecker and QuickTest checker satisfy. +// Useful for enabling test setup functions to fail without a panic. +type SimpleTester interface { + Fatalf(format string, args ...interface{}) + Logf(format string, args ...interface{}) +} + // An Authenticator is an implementation of jimm.Authenticator that returns // the stored user and error. type Authenticator struct { @@ -49,19 +57,22 @@ func (m MockOAuthAuthenticator) VerifySessionToken(token string, secretKey strin return auth.VerifySessionToken(token, m.secretKey) } -func NewUserSessionLogin(username string) api.LoginProvider { +// NewUserSessionLogin returns a login provider than be used with Juju Dial Opts +// to define how login will take place. In this case we login using a session token +// that the JIMM server should verify with the same test secret. +func NewUserSessionLogin(c SimpleTester, username string) api.LoginProvider { email := convertUsernameToEmail(username) token, err := jwt.NewBuilder(). Subject(email). Expiration(time.Now().Add(1 * time.Hour)). Build() if err != nil { - panic("failed to generate test session token") + c.Fatalf("failed to generate test session token") } freshToken, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte(jwtTestSecret))) if err != nil { - panic("failed to sign test session token") + c.Fatalf("failed to sign test session token") } b64Token := base64.StdEncoding.EncodeToString(freshToken) diff --git a/internal/jujuapi/websocket_test.go b/internal/jujuapi/websocket_test.go index 087accfc1..e0fb1bb24 100644 --- a/internal/jujuapi/websocket_test.go +++ b/internal/jujuapi/websocket_test.go @@ -117,7 +117,7 @@ func (s *websocketSuite) openNoAssert(c *gc.C, info *api.Info, username string) c.Assert(err, gc.Equals, nil) inf.CACert = w.String() - lp := jimmtest.NewUserSessionLogin(username) + lp := jimmtest.NewUserSessionLogin(c, username) return api.Open(&inf, api.DialOpts{ InsecureSkipVerify: true, diff --git a/service.go b/service.go index 3d152c5fd..a73b5505e 100644 --- a/service.go +++ b/service.go @@ -91,7 +91,6 @@ type Params struct { // ControllerAdmins contains a list of users (or groups) // that will be given the access-level "superuser" when they // authenticate to the controller. - // TODO(CSS-7507) - Wire this up for OAuth bootstrapping. ControllerAdmins []string // DisableConnectionCache disables caching connections to diff --git a/service_test.go b/service_test.go index 5fdd9fe8c..5d28b5801 100644 --- a/service_test.go +++ b/service_test.go @@ -112,7 +112,7 @@ func TestAuthenticator(t *testing.T) { } conn, err := api.Open(&info, api.DialOpts{ - LoginProvider: jimmtest.NewUserSessionLogin("alice"), + LoginProvider: jimmtest.NewUserSessionLogin(c, "alice"), InsecureSkipVerify: true, }) c.Assert(err, qt.IsNil) @@ -127,7 +127,7 @@ func TestAuthenticator(t *testing.T) { c.Check(conn.ControllerAccess(), qt.Equals, "") conn, err = api.Open(&info, api.DialOpts{ - LoginProvider: jimmtest.NewUserSessionLogin("bob"), + LoginProvider: jimmtest.NewUserSessionLogin(c, "bob"), InsecureSkipVerify: true, }) c.Assert(err, qt.IsNil) @@ -186,7 +186,7 @@ func TestVault(t *testing.T) { } conn, err := api.Open(&info, api.DialOpts{ - LoginProvider: jimmtest.NewUserSessionLogin("bob"), + LoginProvider: jimmtest.NewUserSessionLogin(c, "bob"), InsecureSkipVerify: true, }) c.Assert(err, qt.IsNil) @@ -273,7 +273,7 @@ func TestOpenFGA(t *testing.T) { } conn, err := api.Open(&info, api.DialOpts{ - LoginProvider: jimmtest.NewUserSessionLogin("bob"), + LoginProvider: jimmtest.NewUserSessionLogin(c, "bob"), InsecureSkipVerify: true, }) c.Assert(err, qt.IsNil) From 4bb9df339f2459ce48142f504de4a7c1df9aaf5d Mon Sep 17 00:00:00 2001 From: ale8k Date: Mon, 18 Mar 2024 14:13:00 +0000 Subject: [PATCH 077/126] Test cases --- go.mod | 3 +- go.sum | 4 + internal/auth/oauth2.go | 20 +- internal/auth/oauth2_test.go | 248 +++++++++++++++++++++++-- internal/jimmhttp/auth_handler.go | 5 - internal/jimmhttp/auth_handler_test.go | 20 +- internal/jimmtest/auth.go | 7 +- internal/jujuapi/admin.go | 2 +- internal/jujuapi/admin_test.go | 33 +++- service.go | 2 +- 10 files changed, 287 insertions(+), 57 deletions(-) diff --git a/go.mod b/go.mod index d931672c5..a199906cf 100644 --- a/go.mod +++ b/go.mod @@ -135,7 +135,7 @@ require ( github.com/godbus/dbus/v5 v5.0.4 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.0.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect @@ -255,6 +255,7 @@ require ( github.com/muhlemmer/gu v0.3.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/oracle/oci-go-sdk/v65 v65.55.0 // indirect github.com/packethost/packngo v0.28.1 // indirect diff --git a/go.sum b/go.sum index b53d035bd..97d708e7b 100644 --- a/go.sum +++ b/go.sum @@ -302,6 +302,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -889,6 +891,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= +github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk= +github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index d532f2d09..059c237e3 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -119,6 +119,14 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP return nil, errors.E(op, errors.CodeServerConfiguration, err, "failed to create oidc provider") } + if params.SessionCookieMaxAge == 0 { + return nil, errors.E(op, errors.CodeServerConfiguration, err, "session cookie max age not set") + } + + if params.SessionTokenExpiry == 0 { + return nil, errors.E(op, errors.CodeServerConfiguration, err, "session token expiry not set") + } + return &AuthenticationService{ provider: provider, oauthConfig: oauth2.Config{ @@ -384,7 +392,7 @@ func (as *AuthenticationService) CreateBrowserSession( secureCookies bool, email string, ) error { - const op = errors.Op("") + const op = errors.Op("auth.AuthenticationService.CreateBrowserSession") session, err := as.sessionStore.Get(r, SessionName) if err != nil { @@ -407,12 +415,12 @@ func (as *AuthenticationService) CreateBrowserSession( // retrieving new access tokens upon expiry. If this cannot be done, the cookie // is deleted and an error is returned. func (as *AuthenticationService) AuthenticateBrowserSession(ctx context.Context, w http.ResponseWriter, req *http.Request) (context.Context, error) { - const op = errors.Op("") + const op = errors.Op("auth.AuthenticationService.AuthenticateBrowserSession") // Get the session for this cookie session, err := as.sessionStore.Get(req, SessionName) if err != nil { - return ctx, errors.E(op, err) + return ctx, errors.E(op, err, "failed to retrieve session") } // Get the identity id (email) @@ -424,7 +432,7 @@ func (as *AuthenticationService) AuthenticateBrowserSession(ctx context.Context, // If it's not ok, kill their session if err != nil { session.Options.MaxAge = -1 - if err = session.Save(req, w); err != nil { + if err := session.Save(req, w); err != nil { return ctx, errors.E(op, err) } return ctx, errors.E(op, err) @@ -444,7 +452,7 @@ func (as *AuthenticationService) AuthenticateBrowserSession(ctx context.Context, // validateAndUpdateAccessToken func (as *AuthenticationService) validateAndUpdateAccessToken(ctx context.Context, email any) error { - const op = errors.Op("") + const op = errors.Op("auth.AuthenticationService.validateAndUpdateAccessToken") // Cast the email, it is any because we pass it through the context when authenticating // with cookies and it makes sense to handle the casting here @@ -489,7 +497,7 @@ func (as *AuthenticationService) validateAndUpdateAccessToken(ctx context.Contex // // This is to be called only when a token is expired. func (as *AuthenticationService) refreshIdentitiesToken(ctx context.Context, email string, t *oauth2.Token) error { - const op = errors.Op("") + const op = errors.Op("auth.AuthenticationService.refreshIdentitiesToken") tSrc := as.oauthConfig.TokenSource(ctx, t) diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 96e5c219e..835d99ec0 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -4,10 +4,12 @@ package auth_test import ( "context" + "encoding/base64" "fmt" "io" "net/http" "net/http/cookiejar" + "net/http/httptest" "net/url" "regexp" "testing" @@ -20,9 +22,10 @@ import ( "github.com/canonical/jimm/internal/jimmtest" "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" + "github.com/gorilla/sessions" ) -func setupTestAuthSvc(ctx context.Context, c *qt.C, expiry time.Duration) (*auth.AuthenticationService, *db.Database) { +func setupTestAuthSvc(ctx context.Context, c *qt.C, expiry time.Duration) (*auth.AuthenticationService, *db.Database, sessions.Store) { db := &db.Database{ DB: jimmtest.PostgresDB(c, func() time.Time { return time.Now() }), } @@ -35,18 +38,19 @@ func setupTestAuthSvc(ctx context.Context, c *qt.C, expiry time.Duration) (*auth c.Assert(err, qt.IsNil) authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: expiry, - RedirectURL: "http://localhost:8080/auth/callback", - Store: db, - SessionStore: sessionStore, + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: expiry, + RedirectURL: "http://localhost:8080/auth/callback", + Store: db, + SessionStore: sessionStore, + SessionCookieMaxAge: 60, }) c.Assert(err, qt.IsNil) - return authSvc, db + return authSvc, db, sessionStore } // This test requires the local docker compose to be running and keycloak @@ -57,7 +61,7 @@ func TestAuthCodeURL(t *testing.T) { c := qt.New(t) ctx := context.Background() - authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, _, _ := setupTestAuthSvc(ctx, c, time.Hour) url := authSvc.AuthCodeURL() c.Assert( @@ -83,7 +87,7 @@ func TestDevice(t *testing.T) { ctx := context.Background() - authSvc, db := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, db, _ := setupTestAuthSvc(ctx, c, time.Hour) res, err := authSvc.Device(ctx) c.Assert(err, qt.IsNil) @@ -174,7 +178,7 @@ func TestSessionTokens(t *testing.T) { ctx := context.Background() - authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, _, _ := setupTestAuthSvc(ctx, c, time.Hour) secretKey := "secret-key" token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) @@ -191,7 +195,7 @@ func TestSessionTokenRejectsWrongSecretKey(t *testing.T) { ctx := context.Background() - authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, _, _ := setupTestAuthSvc(ctx, c, time.Hour) secretKey := "secret-key" token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) @@ -208,7 +212,7 @@ func TestSessionTokenRejectsExpiredToken(t *testing.T) { ctx := context.Background() noDuration := time.Duration(0) - authSvc, _ := setupTestAuthSvc(ctx, c, noDuration) + authSvc, _, _ := setupTestAuthSvc(ctx, c, noDuration) secretKey := "secret-key" token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) @@ -224,7 +228,7 @@ func TestSessionTokenValidatesEmail(t *testing.T) { ctx := context.Background() - authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, _, _ := setupTestAuthSvc(ctx, c, time.Hour) secretKey := "secret-key" token, err := authSvc.MintSessionToken("", secretKey) @@ -245,7 +249,7 @@ func TestVerifyClientCredentials(t *testing.T) { validClientSecret = "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf" ) - authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, _, _ := setupTestAuthSvc(ctx, c, time.Hour) err := authSvc.VerifyClientCredentials(ctx, validClientID, validClientSecret) c.Assert(err, qt.IsNil) @@ -253,3 +257,213 @@ func TestVerifyClientCredentials(t *testing.T) { err = authSvc.VerifyClientCredentials(ctx, "invalid-client-id", validClientSecret) c.Assert(err, qt.ErrorMatches, "invalid client credentials") } + +func assertSetCookiesIsCorrect(c *qt.C, rec *httptest.ResponseRecorder, parsedCookies []*http.Cookie) { + assertHasCookie := func(name string, cookies []*http.Cookie) { + found := false + for _, v := range cookies { + if v.Name == name { + found = true + } + } + c.Assert(found, qt.IsTrue) + } + assertHasCookie(auth.SessionName, parsedCookies) + assertHasCookie("Path", parsedCookies) + assertHasCookie("Expires", parsedCookies) + assertHasCookie("Max-Age", parsedCookies) +} + +func TestCreateBrowserSession(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + + authSvc, _, sessionStore := setupTestAuthSvc(ctx, c, time.Hour) + + rec := httptest.NewRecorder() + req, err := http.NewRequest("GET", "", nil) + c.Assert(err, qt.IsNil) + + err = authSvc.CreateBrowserSession(ctx, rec, req, false, "jimm-test@canonical.com") + c.Assert(err, qt.IsNil) + + cookies := rec.Header().Get("Set-Cookie") + parsedCookies := jimmtest.ParseCookies(cookies) + assertSetCookiesIsCorrect(c, rec, parsedCookies) + + req.AddCookie(&http.Cookie{ + Name: auth.SessionName, + Value: parsedCookies[0].Value, + }) + + session, err := sessionStore.Get(req, auth.SessionName) + c.Assert(err, qt.IsNil) + c.Assert(session.Values[auth.SessionIdentityKey], qt.Equals, "jimm-test@canonical.com") +} + +func TestAuthenticateBrowserSession(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + + authSvc, db, sessionStore := setupTestAuthSvc(ctx, c, time.Hour) + + cookie, err := jimmtest.RunBrowserLogin(db, sessionStore) + c.Assert(err, qt.IsNil) + + rec := httptest.NewRecorder() + req, err := http.NewRequest("GET", "", nil) + c.Assert(err, qt.IsNil) + + cookies := jimmtest.ParseCookies(cookie) + + req.AddCookie(cookies[0]) + + ctx, err = authSvc.AuthenticateBrowserSession(ctx, rec, req) + c.Assert(err, qt.IsNil) + + // Check identity added + identityId := auth.SessionIdentityFromContext(ctx) + c.Assert(identityId, qt.Equals, "jimm-test@canonical.com") + + // Assert Set-Cookie present + setCookieCookies := rec.Header().Get("Set-Cookie") + parsedCookies := jimmtest.ParseCookies(setCookieCookies) + assertSetCookiesIsCorrect(c, rec, parsedCookies) +} + +func TestAuthenticateBrowserSessionRejectsNoneDecryptableOrDecodableCookies(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + + authSvc, db, sessionStore := setupTestAuthSvc(ctx, c, time.Hour) + + _, err := jimmtest.RunBrowserLogin(db, sessionStore) + c.Assert(err, qt.IsNil) + + // Failure case 1: Bad base64 decoding + req, err := http.NewRequest("GET", "", nil) + c.Assert(err, qt.IsNil) + req.AddCookie(&http.Cookie{ + Name: auth.SessionName, + Value: "bad cookie, very naughty, bad bad cookie", + }) + + rec := httptest.NewRecorder() + + // The underlying error is a failed base64 decode + _, err = authSvc.AuthenticateBrowserSession(ctx, rec, req) + c.Assert(err, qt.ErrorMatches, "failed to retrieve session") + + // Failure case 2: Value isn't valid but is base64 decoded + req, err = http.NewRequest("GET", "", nil) + c.Assert(err, qt.IsNil) + req.AddCookie(&http.Cookie{ + Name: auth.SessionName, + Value: base64.StdEncoding.EncodeToString([]byte("bad cookie, very naughty, bad bad cookie")), + }) + + rec = httptest.NewRecorder() + + // The underlying error is a a value not valid err + _, err = authSvc.AuthenticateBrowserSession(ctx, rec, req) + c.Assert(err, qt.ErrorMatches, "failed to retrieve session") +} + +func TestAuthenticateBrowserSessionHandlesExpiredAccessTokens(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + + authSvc, db, sessionStore := setupTestAuthSvc(ctx, c, time.Hour) + + cookie, err := jimmtest.RunBrowserLogin(db, sessionStore) + c.Assert(err, qt.IsNil) + + rec := httptest.NewRecorder() + req, err := http.NewRequest("GET", "", nil) + c.Assert(err, qt.IsNil) + + cookies := jimmtest.ParseCookies(cookie) + + req.AddCookie(cookies[0]) + + // User exists from run browser login, but we're gonna + // artificially expire their access token + u := dbmodel.Identity{ + Name: "jimm-test@canonical.com", + } + err = db.GetIdentity(ctx, &u) + c.Assert(err, qt.IsNil) + + previousToken := u.AccessToken + + u.AccessTokenExpiry = time.Now() + db.UpdateIdentity(ctx, &u) + + ctx, err = authSvc.AuthenticateBrowserSession(ctx, rec, req) + c.Assert(err, qt.IsNil) + + // Check identity added + identityId := auth.SessionIdentityFromContext(ctx) + c.Assert(identityId, qt.Equals, "jimm-test@canonical.com") + // Check recorder has Set-Cookie + + // Get identity again with new access token expiry and access token + err = db.GetIdentity(ctx, &u) + c.Assert(err, qt.IsNil) + + // Assert new access token is valid for at least 4 minutes(our setup is 5 minutes) + c.Assert(u.AccessTokenExpiry.After(time.Now().Add(time.Minute*4)), qt.IsTrue) + // Assert its not the same token as previous token + c.Assert(u.AccessToken, qt.Not(qt.Equals), previousToken) + // Assert Set-Cookie present + setCookieCookies := rec.Header().Get("Set-Cookie") + parsedCookies := jimmtest.ParseCookies(setCookieCookies) + assertSetCookiesIsCorrect(c, rec, parsedCookies) +} + +func TestAuthenticateBrowserSessionHandlesMissingOrExpiredRefreshTokens(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + + authSvc, db, sessionStore := setupTestAuthSvc(ctx, c, time.Hour) + + cookie, err := jimmtest.RunBrowserLogin(db, sessionStore) + c.Assert(err, qt.IsNil) + + rec := httptest.NewRecorder() + req, err := http.NewRequest("GET", "", nil) + c.Assert(err, qt.IsNil) + + cookies := jimmtest.ParseCookies(cookie) + + req.AddCookie(cookies[0]) + + // User exists from run browser login, but we're gonna + // artificially expire their access token + u := dbmodel.Identity{ + Name: "jimm-test@canonical.com", + } + err = db.GetIdentity(ctx, &u) + c.Assert(err, qt.IsNil) + + // As our access token has "expired" + u.AccessTokenExpiry = time.Now() + // And we're missing a refresh token (the same case would apply for an expired refresh token + // or any scenario where the token source cannot refresh the access token) + u.RefreshToken = "" + db.UpdateIdentity(ctx, &u) + + // AuthenticateBrowserSession should fail to refresh the users session and delete + // the current session, giving us the same cookie back with a max-age of -1. + _, err = authSvc.AuthenticateBrowserSession(ctx, rec, req) + c.Assert(err, qt.ErrorMatches, ".*failed to refresh token.*") + + // Assert that the header to delete the session is set correctly based + // on a failed access token refresh due to refresh token issues. + setCookieCookies := rec.Header().Get("Set-Cookie") + c.Assert( + setCookieCookies, + qt.Equals, + "jimm-browser-session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0", + ) +} diff --git a/internal/jimmhttp/auth_handler.go b/internal/jimmhttp/auth_handler.go index 461304d63..43eb15156 100644 --- a/internal/jimmhttp/auth_handler.go +++ b/internal/jimmhttp/auth_handler.go @@ -20,7 +20,6 @@ type OAuthHandler struct { authenticator BrowserOAuthAuthenticator dashboardFinalRedirectURL string secureCookies bool - cookieExpiry int } // OAuthHandlerParams holds the parameters to configure the OAuthHandler. @@ -35,9 +34,6 @@ type OAuthHandlerParams struct { // SessionCookies determines if HTTPS must be enabled in order for JIMM // to set cookies when creating browser based sessions. SecureCookies bool - - // CookieExpiry is how long the cookie will be valid before expiring in seconds. - CookieExpiry int } // BrowserOAuthAuthenticator handles authorisation code authentication within JIMM @@ -70,7 +66,6 @@ func NewOAuthHandler(p OAuthHandlerParams) (*OAuthHandler, error) { authenticator: p.Authenticator, dashboardFinalRedirectURL: p.DashboardFinalRedirectURL, secureCookies: p.SecureCookies, - cookieExpiry: p.CookieExpiry, }, nil } diff --git a/internal/jimmhttp/auth_handler_test.go b/internal/jimmhttp/auth_handler_test.go index d45954264..190da8701 100644 --- a/internal/jimmhttp/auth_handler_test.go +++ b/internal/jimmhttp/auth_handler_test.go @@ -40,30 +40,16 @@ func TestBrowserAuth(t *testing.T) { c := qt.New(t) db, sessionStore := setupDbAndSessionStore(c) - cookie, err := jimmtest.RunBrowserLogin(db, sessionStore, 60) + cookie, err := jimmtest.RunBrowserLogin(db, sessionStore) c.Assert(err, qt.IsNil) c.Assert(cookie, qt.Not(qt.Equals), "") - - // // Get the decrypted session by falseifying (cant spell) the request - // r, _ := http.NewRequest("", "", nil) - // r.Header.Set("Cookie", cookie) - // session, err := sessionStore.Get(r, auth.SessionName) - // c.Assert(err, qt.IsNil) - // fmt.Println(session) - - // // Get the raw cookies by falseifying a save and retrieving set-cookie header - // w := httptest.NewRecorder() - // session.Options.MaxAge = 30 - // session.Save(r, w) - // decryptedCookie := w.Header().Get("Set-Cookie") - // c.Assert(decryptedCookie, qt.Equals, "digsdig") } func TestCallbackFailsNoCodePresent(t *testing.T) { c := qt.New(t) db, sessionStore := setupDbAndSessionStore(c) - s, err := jimmtest.SetupTestDashboardCallbackHandler("", db, sessionStore, 60) + s, err := jimmtest.SetupTestDashboardCallbackHandler("", db, sessionStore) c.Assert(err, qt.IsNil) defer s.Close() @@ -82,7 +68,7 @@ func TestCallbackFailsExchange(t *testing.T) { c := qt.New(t) db, sessionStore := setupDbAndSessionStore(c) - s, err := jimmtest.SetupTestDashboardCallbackHandler("", db, sessionStore, 60) + s, err := jimmtest.SetupTestDashboardCallbackHandler("", db, sessionStore) c.Assert(err, qt.IsNil) defer s.Close() diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index 30a4e5f99..bdb60bf7f 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -91,7 +91,7 @@ func convertUsernameToEmail(username string) string { return username } -func SetupTestDashboardCallbackHandler(browserURL string, db *db.Database, sessionStore sessions.Store, cookieExpiry int) (*httptest.Server, error) { +func SetupTestDashboardCallbackHandler(browserURL string, db *db.Database, sessionStore sessions.Store) (*httptest.Server, error) { // Create unstarted server to enable auth service s := httptest.NewUnstartedServer(nil) // Setup random port listener @@ -129,7 +129,6 @@ func SetupTestDashboardCallbackHandler(browserURL string, db *db.Database, sessi Authenticator: authSvc, DashboardFinalRedirectURL: browserURL, SecureCookies: false, - CookieExpiry: cookieExpiry, }) if err != nil { return nil, err @@ -147,7 +146,7 @@ func SetupTestDashboardCallbackHandler(browserURL string, db *db.Database, sessi return s, nil } -func RunBrowserLogin(db *db.Database, sessionStore sessions.Store, cookieExpiry int) (string, error) { +func RunBrowserLogin(db *db.Database, sessionStore sessions.Store) (string, error) { var cookieString string // Setup final test redirect url server, to emulate @@ -163,7 +162,7 @@ func RunBrowserLogin(db *db.Database, sessionStore sessions.Store, cookieExpiry ) defer browser.Close() - s, err := SetupTestDashboardCallbackHandler(browser.URL, db, sessionStore, cookieExpiry) + s, err := SetupTestDashboardCallbackHandler(browser.URL, db, sessionStore) if err != nil { return cookieString, err } diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index 6005db250..51b65fb75 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -107,7 +107,7 @@ func (r *controllerRoot) LoginWithSessionCookie(ctx context.Context) (jujuparams // If no identity ID has come through, then no cookie was present // and as such authentication has failed. if r.identityId == "" { - return jujuparams.LoginResult{}, errors.E(op, (&auth.AuthenticationError{}).Error()) + return jujuparams.LoginResult{}, errors.E(op, &auth.AuthenticationError{}) } user, err := r.jimm.GetOpenFGAUserAndAuthorise(ctx, r.identityId) diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index cf7f0e9da..72b60b116 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -82,6 +82,11 @@ func (s *adminSuite) TestLoginToController(c *gc.C) { // // Within the test are clear comments explaining what is happening when and why. // Please refer to these comments for further details. +// +// We only test happy path here due to having tested edge cases and failure cases +// within the auth service itself such as invalid cookies, expired access tokens and +// missing/expired/revoked refresh tokens. + func (s *adminSuite) TestBrowserLogin(c *gc.C) { // The setup runs a browser login with callback, ultimately retrieving // a logged in user by cookie. @@ -91,7 +96,7 @@ func (s *adminSuite) TestBrowserLogin(c *gc.C) { sessionStore, err := pgstore.NewPGStoreFromPool(sqldb, []byte("secretsecretdigletts")) c.Assert(err, gc.IsNil) - cookie, err := jimmtest.RunBrowserLogin(s.JIMM.DB(), sessionStore, 60) + cookie, err := jimmtest.RunBrowserLogin(s.JIMM.DB(), sessionStore) c.Assert(err, gc.IsNil) c.Assert(cookie, gc.Not(gc.Equals), "") @@ -158,15 +163,33 @@ func (s *adminSuite) TestBrowserLogin(c *gc.C) { defer conn.Close() lr := &jujuparams.LoginResult{} - c.Assert( - conn.APICall("Admin", 4, "", "LoginWithSessionCookie", nil, lr), - gc.IsNil, - ) + err = conn.APICall("Admin", 4, "", "LoginWithSessionCookie", nil, lr) + c.Assert(err, gc.IsNil) c.Assert(lr.UserInfo.Identity, gc.Equals, "user-jimm-test@canonical.com") c.Assert(lr.UserInfo.DisplayName, gc.Equals, "jimm-test") } +// TestBrowserLoginNoCookie attempts to login without a cookie. +func (s *adminSuite) TestBrowserLoginNoCookie(c *gc.C) { + conn := s.open( + c, + &api.Info{ + SkipLogin: true, + }, + "test", + ) + defer conn.Close() + + lr := &jujuparams.LoginResult{} + err := conn.APICall("Admin", 4, "", "LoginWithSessionCookie", nil, lr) + c.Assert( + err, + gc.ErrorMatches, + "authentication failed", + ) +} + // TestDeviceLogin takes a test user through the flow of logging into jimm // via the correct facades. All are done in a single test to see the flow end-2-end. // diff --git a/service.go b/service.go index 38c4de6cc..12e4360d2 100644 --- a/service.go +++ b/service.go @@ -263,6 +263,7 @@ func NewService(ctx context.Context, p Params) (*Service, error) { if err != nil { return nil, errors.E(op, err) } + // Cleanup expired session every 30 minutes defer sessionStore.StopCleanup(sessionStore.Cleanup(time.Minute * 30)) @@ -353,7 +354,6 @@ func NewService(ctx context.Context, p Params) (*Service, error) { Authenticator: authSvc, DashboardFinalRedirectURL: p.DashboardFinalRedirectURL, SecureCookies: p.SecureSessionCookies, - CookieExpiry: p.SessionCookieExpiry, }) if err != nil { return nil, errors.E(op, err, "failed to setup authentication handler") From cbea4135b3f96de6e8c1c81fd1029ea414b90963 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:24:30 +0200 Subject: [PATCH 078/126] Update test Postgres DB name (#1173) * Update gorm.go * Use self-hosted runners * Revert self-hosted runners * Fix remove mongodb step --- .github/workflows/ci.yaml | 4 +--- internal/jimmtest/gorm.go | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 83b1e483d..9f6f5e0b8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,7 +24,7 @@ jobs: build_test: name: Build and Test - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 with: @@ -35,8 +35,6 @@ jobs: go-version-file: 'go.mod' - name: Install dependencies run: sudo apt-get update -y && sudo apt-get install -y gcc git-core gnupg build-essential - - name: Remove installed mongodb - run: sudo apt purge mongodb-org && sudo apt autoremove - run: sudo snap install juju-db --channel 4.4/stable - name: Add volume files run: | diff --git a/internal/jimmtest/gorm.go b/internal/jimmtest/gorm.go index e1522bc3a..3e8d9b1df 100644 --- a/internal/jimmtest/gorm.go +++ b/internal/jimmtest/gorm.go @@ -8,6 +8,7 @@ import ( "crypto/sha1" "encoding/base64" "fmt" + "math/rand" "net/url" "os" "regexp" @@ -148,7 +149,9 @@ func computeSafeDatabaseName(suggestedName string) string { safeName := re.ReplaceAllString(suggestedName, "_") hasher := sha1.New() - hasher.Write([]byte(suggestedName)) + // Provide some random chars for the hash. Useful where tests + // have the same suite name and same test name. + hasher.Write([]byte(suggestedName + randSeq(5))) sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) // Note that when using `base64.URLEncoding` the result may include a hyphen (-) @@ -165,6 +168,16 @@ func computeSafeDatabaseName(suggestedName string) string { return strings.ToLower(safeName[:maxDatabaseNameLength-len(shaSuffix)] + shaSuffix) } +var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +func randSeq(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} + // createDatabaseMutex to avoid issues at the time of creating databases, it's // best to synchronize them to happen sequentially (specially, when creating a // database from a template). From 56784b93790cac1b308ca724e5360f410d691894 Mon Sep 17 00:00:00 2001 From: ale8k Date: Tue, 19 Mar 2024 08:53:08 +0000 Subject: [PATCH 079/126] update service tests --- cmd/jimmsrv/main.go | 12 +++--- internal/auth/oauth2.go | 2 +- internal/cmdtest/jimmsuite.go | 9 +++-- internal/jimmjwx/utils_test.go | 9 +++-- service.go | 20 +++++----- service_test.go | 72 +++++++++++++++++++--------------- 6 files changed, 67 insertions(+), 57 deletions(-) diff --git a/cmd/jimmsrv/main.go b/cmd/jimmsrv/main.go index 37079c200..7a1e14cc0 100644 --- a/cmd/jimmsrv/main.go +++ b/cmd/jimmsrv/main.go @@ -159,15 +159,15 @@ func start(ctx context.Context, s *service.Service) error { JWTExpiryDuration: jwtExpiryDuration, InsecureSecretStorage: insecureSecretStorage, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: issuerURL, - ClientID: clientID, - ClientSecret: clientSecret, - Scopes: scopesParsed, - SessionTokenExpiry: sessionTokenExpiryDuration, + IssuerURL: issuerURL, + ClientID: clientID, + ClientSecret: clientSecret, + Scopes: scopesParsed, + SessionTokenExpiry: sessionTokenExpiryDuration, + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: os.Getenv("JIMM_DASHBOARD_FINAL_REDIRECT_URL"), SecureSessionCookies: secureSessionCookies, - SessionCookieExpiry: sessionCookieExpiryInt, }) if err != nil { return err diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 059c237e3..b669ed35f 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -92,7 +92,7 @@ type AuthenticationServiceParams struct { Scopes []string // SessionTokenExpiry holds the expiry time of minted JIMM session tokens (JWTs). SessionTokenExpiry time.Duration - // sessionCookieMaxAge holds the max age for session cookies. + // SessionCookieMaxAge holds the max age for session cookies. SessionCookieMaxAge int // RedirectURL is the URL for handling the exchange of authorisation // codes into access tokens (and id tokens), for JIMM, this is expected diff --git a/internal/cmdtest/jimmsuite.go b/internal/cmdtest/jimmsuite.go index bc90bc5ec..5b31f7810 100644 --- a/internal/cmdtest/jimmsuite.go +++ b/internal/cmdtest/jimmsuite.go @@ -83,10 +83,11 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { JWTExpiryDuration: time.Minute, InsecureSecretStorage: true, OAuthAuthenticatorParams: service.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", } diff --git a/internal/jimmjwx/utils_test.go b/internal/jimmjwx/utils_test.go index 642a776b7..3e31b7537 100644 --- a/internal/jimmjwx/utils_test.go +++ b/internal/jimmjwx/utils_test.go @@ -109,10 +109,11 @@ func setupService(ctx context.Context, c *qt.C) (*jimm.Service, *httptest.Server AuthModel: cofgaParams.AuthModelID, }, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", }) diff --git a/service.go b/service.go index a1f5eeaf2..dfaee252f 100644 --- a/service.go +++ b/service.go @@ -74,6 +74,8 @@ type OAuthAuthenticatorParams struct { // SessionTokenExpiry holds the expiry duration for issued JWTs // for user (CLI) to JIMM authentication. SessionTokenExpiry time.Duration + // SessionCookieMaxAge holds the max age for session cookies. + SessionCookieMaxAge int } // A Params structure contains the parameters required to initialise a new @@ -167,9 +169,6 @@ type Params struct { // SecureSessionCookies determines if HTTPS must be enabled in order for JIMM // to set cookies when creating browser based sessions. SecureSessionCookies bool - - // SessionCookieExpiry is how long the cookie will be valid before expiring in seconds. - SessionCookieExpiry int } // A Service is the implementation of a JIMM server. @@ -291,13 +290,14 @@ func NewService(ctx context.Context, p Params) (*Service, error) { authSvc, err := auth.NewAuthenticationService( ctx, auth.AuthenticationServiceParams{ - IssuerURL: p.OAuthAuthenticatorParams.IssuerURL, - ClientID: p.OAuthAuthenticatorParams.ClientID, - ClientSecret: p.OAuthAuthenticatorParams.ClientSecret, - Scopes: p.OAuthAuthenticatorParams.Scopes, - SessionTokenExpiry: p.OAuthAuthenticatorParams.SessionTokenExpiry, - Store: &s.jimm.Database, - SessionStore: sessionStore, + IssuerURL: p.OAuthAuthenticatorParams.IssuerURL, + ClientID: p.OAuthAuthenticatorParams.ClientID, + ClientSecret: p.OAuthAuthenticatorParams.ClientSecret, + Scopes: p.OAuthAuthenticatorParams.Scopes, + SessionTokenExpiry: p.OAuthAuthenticatorParams.SessionTokenExpiry, + SessionCookieMaxAge: p.OAuthAuthenticatorParams.SessionCookieMaxAge, + Store: &s.jimm.Database, + SessionStore: sessionStore, }, ) s.jimm.OAuthAuthenticator = authSvc diff --git a/service_test.go b/service_test.go index 5d28b5801..c40cd8047 100644 --- a/service_test.go +++ b/service_test.go @@ -47,10 +47,11 @@ func TestDefaultService(t *testing.T) { OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), InsecureSecretStorage: true, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", }) @@ -72,10 +73,11 @@ func TestServiceStartsWithoutSecretStore(t *testing.T) { DSN: jimmtest.CreateEmptyDatabase(c), OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", }) @@ -95,10 +97,11 @@ func TestAuthenticator(t *testing.T) { OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), InsecureSecretStorage: true, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", } @@ -164,10 +167,11 @@ func TestVault(t *testing.T) { VaultSecretFile: "./local/vault/approle.json", OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", } @@ -233,10 +237,11 @@ func TestPostgresSecretStore(t *testing.T) { OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), InsecureSecretStorage: true, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", } @@ -256,10 +261,11 @@ func TestOpenFGA(t *testing.T) { OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), ControllerAdmins: []string{"alice", "eve"}, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", } @@ -312,10 +318,11 @@ func TestPublicKey(t *testing.T) { PrivateKey: "c1VkV05+iWzCxMwMVcWbr0YJWQSEO62v+z3EQ2BhFMw=", PublicKey: "pC8MEk9MS9S8fhyRnOJ4qARTcTAwoM9L1nH/Yq0MwWU=", OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", } @@ -400,10 +407,11 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { PrivateKey: "c1VkV05+iWzCxMwMVcWbr0YJWQSEO62v+z3EQ2BhFMw=", PublicKey: "pC8MEk9MS9S8fhyRnOJ4qARTcTAwoM9L1nH/Yq0MwWU=", OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", } From 1c0610d331e22ecc07f1d3b38a33e7a7fb9ad4c7 Mon Sep 17 00:00:00 2001 From: ale8k Date: Tue, 19 Mar 2024 09:25:08 +0000 Subject: [PATCH 080/126] Fix test --- internal/jimm/user_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/jimm/user_test.go b/internal/jimm/user_test.go index 6dd6ed31a..bae5f7b52 100644 --- a/internal/jimm/user_test.go +++ b/internal/jimm/user_test.go @@ -35,14 +35,14 @@ func TestGetOpenFGAUser(t *testing.T) { sessionStore, err := pgstore.NewPGStoreFromPool(sqldb, []byte("secretsecretdigletts")) c.Assert(err, qt.IsNil) - // TODO(ale8k): Mock this authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{"openid", "profile", "email"}, - SessionTokenExpiry: time.Hour, - Store: db, - SessionStore: sessionStore, + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{"openid", "profile", "email"}, + SessionTokenExpiry: time.Hour, + Store: db, + SessionStore: sessionStore, + SessionCookieMaxAge: 60, }) c.Assert(err, qt.IsNil) From ef3e14f107059b664601c54b752453bb4e521f75 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Tue, 19 Mar 2024 09:39:34 +0000 Subject: [PATCH 081/126] CSS-7081 Add OAuth-specific methods to secrets store (#1175) * Add `Get/Put` OAuth key methods to Vault store Signed-off-by: Babak K. Shandiz * Add `Get/Put` OAuth key methods to Postgres store Signed-off-by: Babak K. Shandiz * Add `Get/Put` OAuth key methods to in-memory store Signed-off-by: Babak K. Shandiz * Add `Get/Put` OAuth key methods to mock store Signed-off-by: Babak K. Shandiz * Add `Get/Put` OAuth key methods to credential store interface Signed-off-by: Babak K. Shandiz * Expose underlying `CredentialStore` via a `JIMM` interface method Signed-off-by: Babak K. Shandiz * Use `New*` method to instantiate in-memory credential store Signed-off-by: Babak K. Shandiz * Use credential store to retrieve OAuth session JWT secret key Signed-off-by: Babak K. Shandiz * Update `CredentialStore` godoc Signed-off-by: Babak K. Shandiz * Add test to verify `GetOAuthKey` returns not found error Signed-off-by: Babak K. Shandiz * Add test to verify `GetOAuthKey` returns not found error Signed-off-by: Babak K. Shandiz * Add `CheckOrGenerateOAuthKey` method Signed-off-by: Babak K. Shandiz * Generate OAuth key on the leader unit Signed-off-by: Babak K. Shandiz * Update suite to generate OAuth key as well Signed-off-by: Babak K. Shandiz * Add package godoc Signed-off-by: Babak K. Shandiz * Reuse shared `JWTTestSecret` in `JimmCmdSuite` Signed-off-by: Babak K. Shandiz * Fix godoc Signed-off-by: Babak K. Shandiz * Add `CleanupOAuth` to Postgres store Signed-off-by: Babak K. Shandiz * Add `CleanupOAuth` to Vault store Signed-off-by: Babak K. Shandiz * Add `CleanupOAuth` to mock store Signed-off-by: Babak K. Shandiz * Add `CleanupOAuth` to in-memory store Signed-off-by: Babak K. Shandiz * Add `CleanupOAuth` to credential store interface Signed-off-by: Babak K. Shandiz * Use same const secret for in-memory store Signed-off-by: Babak K. Shandiz * fix tests with populating OAuth key secrets in store Signed-off-by: Babak K. Shandiz * Use `*WithContext` variants for read/write methods Signed-off-by: Babak K. Shandiz * Use `net.Listen` to find an available TCP port Signed-off-by: Babak K. Shandiz * Rename `CleanupOAuth` to `CleanupOAuthSecrets` Signed-off-by: Babak K. Shandiz * Rename credential store `*OAuthKey` methods to `*OAuthSecret` Signed-off-by: Babak K. Shandiz * Run `go mod tidy` Signed-off-by: Babak K. Shandiz --------- Signed-off-by: Babak K. Shandiz --- cmd/jimmsrv/main.go | 12 ++- go.mod | 7 -- go.sum | 21 ----- internal/cmdtest/jimmsuite.go | 3 + internal/db/export_test.go | 2 + internal/db/secrets.go | 44 +++++++++ internal/db/secrets_test.go | 28 ++++++ internal/jimm/cloudcredential_test.go | 12 +++ internal/jimm/credentials/credentials.go | 12 +++ internal/jimm/jimm.go | 5 + internal/jimm/jimm_test.go | 4 +- internal/jimmhttp/auth_handler_test.go | 17 ++-- internal/jimmtest/auth.go | 6 +- internal/jimmtest/jimm_mock.go | 8 ++ internal/jimmtest/store.go | 44 +++++++++ internal/jimmtest/suite.go | 4 +- internal/jujuapi/admin.go | 18 ++-- internal/jujuapi/controllerroot.go | 2 + internal/vault/vault.go | 114 +++++++++++++++++++---- internal/vault/vault_test.go | 41 ++++++++ service.go | 29 ++++++ service_test.go | 28 ++++-- 22 files changed, 382 insertions(+), 79 deletions(-) diff --git a/cmd/jimmsrv/main.go b/cmd/jimmsrv/main.go index 37079c200..e9918c682 100644 --- a/cmd/jimmsrv/main.go +++ b/cmd/jimmsrv/main.go @@ -179,13 +179,17 @@ func start(ctx context.Context, s *service.Service) error { s.Go(func() error { return jimmsvc.WatchModelSummaries(ctx) }) if os.Getenv("JIMM_ENABLE_JWKS_ROTATOR") != "" { - zapctx.Info(ctx, "attempting to start JWKS rotator") + zapctx.Info(ctx, "attempting to start JWKS rotator and generate OAuth secret key") s.Go(func() error { - err := jimmsvc.StartJWKSRotator(ctx, time.NewTicker(time.Hour).C, time.Now().UTC().AddDate(0, 3, 0)) - if err != nil { + if err := jimmsvc.StartJWKSRotator(ctx, time.NewTicker(time.Hour).C, time.Now().UTC().AddDate(0, 3, 0)); err != nil { zapctx.Error(ctx, "failed to start JWKS rotator", zap.Error(err)) + return err } - return err + if err := jimmsvc.CheckOrGenerateOAuthKey(ctx); err != nil { + zapctx.Error(ctx, "failed to check/generate OAuth secret key", zap.Error(err)) + return err + } + return nil }) } diff --git a/go.mod b/go.mod index d931672c5..a27924975 100644 --- a/go.mod +++ b/go.mod @@ -102,10 +102,7 @@ require ( github.com/aws/smithy-go v1.19.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f // indirect - github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8 // indirect github.com/canonical/lxd v0.0.0-20231214113525-e676fc63c50a // indirect - github.com/canonical/pebble v1.9.0 // indirect - github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b // indirect github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cjlapao/common-go v0.0.39 // indirect @@ -144,7 +141,6 @@ require ( github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/schema v1.2.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.2.1 // indirect @@ -262,7 +258,6 @@ require ( github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/sftp v1.13.6 // indirect - github.com/pkg/term v1.1.0 // indirect github.com/pkg/xattr v0.4.9 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.5.0 // indirect @@ -302,12 +297,10 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.19.0 // indirect golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect - golang.org/x/mod v0.14.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.16.1 // indirect google.golang.org/api v0.154.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect diff --git a/go.sum b/go.sum index b53d035bd..d82c9d791 100644 --- a/go.sum +++ b/go.sum @@ -145,18 +145,12 @@ github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f h1:gOO/tNZMjjvTKZWpY github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= github.com/canonical/go-dqlite v1.21.0 h1:4gLDdV2GF+vg0yv9Ff+mfZZNQ1JGhnQ3GnS2GeZPHfA= github.com/canonical/go-dqlite v1.21.0/go.mod h1:Uvy943N8R4CFUAs59A1NVaziWY9nJ686lScY7ywurfg= -github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8 h1:zGaJEJI9qPVyM+QKFJagiyrM91Ke5S9htoL1D470g6E= -github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8/go.mod h1:ZZFeR9K9iGgpwOaLYF9PdT44/+lfSJ9sQz3B+SsGsYU= github.com/canonical/go-service v1.0.0 h1:TF6TsEp04xAoI5pPoWjTYmEwLjbPATSnHEyeJCvzElg= github.com/canonical/go-service v1.0.0/go.mod h1:GzNLXpkGdglL0kjREXoLXj2rB2Qx+EvAGncRDqCENYQ= github.com/canonical/lxd v0.0.0-20231214113525-e676fc63c50a h1:Tfo/MzXK5GeG7gzSHqxGeY/669Mhh5ea43dn1mRDnk8= github.com/canonical/lxd v0.0.0-20231214113525-e676fc63c50a/go.mod h1:UxfHGKFoRjgu1NUA9EFiR++dKvyAiT0h9HT0ffMlzjc= github.com/canonical/ofga v0.10.0 h1:DHXhG/DAXWWQT/I+2jzr4qm0uTIYrILmtMxd6ZqmEzE= github.com/canonical/ofga v0.10.0/go.mod h1:u4Ou8dbIhO7FmVlT7W3rX2roD9AOGz/CqmGh7AdF0Lo= -github.com/canonical/pebble v1.9.0 h1:FWVEh1fg3aaW2HNue2Z2eYMwkJEQT8mgMFW3R5Iocn4= -github.com/canonical/pebble v1.9.0/go.mod h1:9Qkjmq298g0+9SvM2E5eekkEN4pjHDWhgg9eB2I0tjk= -github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b h1:Da2fardddn+JDlVEYtrzBLTtyzoyU3nIS0Cf0GvjmwU= -github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b/go.mod h1:upTK9n6rlqITN9rCN69hdreI37dRDFUk2thlGGD5Cg8= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= @@ -727,8 +721,6 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kian99/juju v0.0.0-20240301094235-2688d7cd925e h1:MnSWbm0Th+V7YI61C4ledtaxg564bzwBdh665fj/MeM= -github.com/kian99/juju v0.0.0-20240301094235-2688d7cd925e/go.mod h1:V5eSJgiG7Evs4ejKhI7na7olYzHR1rxZXwx1/27Sa18= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -922,8 +914,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= -github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= -github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -1128,8 +1118,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1170,8 +1158,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1220,8 +1206,6 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1291,7 +1275,6 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1320,8 +1303,6 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -1330,8 +1311,6 @@ golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/cmdtest/jimmsuite.go b/internal/cmdtest/jimmsuite.go index bc90bc5ec..50de3118f 100644 --- a/internal/cmdtest/jimmsuite.go +++ b/internal/cmdtest/jimmsuite.go @@ -100,6 +100,9 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { err = s.Service.StartJWKSRotator(ctx, time.NewTicker(time.Hour).C, time.Now().UTC().AddDate(0, 3, 0)) c.Assert(err, gc.Equals, nil) + err = s.JIMM.GetCredentialStore().PutOAuthSecret(ctx, []byte(jimmtest.JWTTestSecret)) + c.Assert(err, gc.Equals, nil) + s.HTTP.StartTLS() // NOW we can set up the juju conn suites diff --git a/internal/db/export_test.go b/internal/db/export_test.go index 2d5d6efba..be98a0c08 100644 --- a/internal/db/export_test.go +++ b/internal/db/export_test.go @@ -7,4 +7,6 @@ var ( JwksPublicKeyTag = jwksPublicKeyTag JwksPrivateKeyTag = jwksPrivateKeyTag JwksExpiryTag = jwksExpiryTag + OAuthKind = oauthKind + OAuthKeyTag = oauthKeyTag ) diff --git a/internal/db/secrets.go b/internal/db/secrets.go index 9cd9b129c..87434599f 100644 --- a/internal/db/secrets.go +++ b/internal/db/secrets.go @@ -26,6 +26,8 @@ const ( jwksPublicKeyTag = "jwksPublicKey" jwksPrivateKeyTag = "jwksPrivateKey" jwksExpiryTag = "jwksExpiry" + oauthKind = "oauth" + oauthKeyTag = "oauthKey" ) // UpsertSecret stores secret information. @@ -280,3 +282,45 @@ func (d *Database) PutJWKSExpiry(ctx context.Context, expiry time.Time) error { secret := dbmodel.NewSecret(jwksKind, jwksExpiryTag, expiryJson) return d.UpsertSecret(ctx, &secret) } + +// CleanupOAuthSecrets removes all secrets associated with OAuth. +func (d *Database) CleanupOAuthSecrets(ctx context.Context) error { + const op = errors.Op("database.CleanupOAuthSecrets") + secret := dbmodel.NewSecret(oauthKind, oauthKeyTag, nil) + err := d.DeleteSecret(ctx, &secret) + if err != nil { + zapctx.Error(ctx, "failed to cleanup OAUth key", zap.Error(err)) + return errors.E(op, err, "failed to cleanup OAUth key") + } + return nil +} + +// GetOAuthSecret returns the current HS256 (symmetric encryption) secret used to sign OAuth session tokens. +func (d *Database) GetOAuthSecret(ctx context.Context) ([]byte, error) { + const op = errors.Op("database.GetOAuthSecret") + secret := dbmodel.NewSecret(oauthKind, oauthKeyTag, nil) + err := d.GetSecret(ctx, &secret) + if err != nil { + zapctx.Error(ctx, "failed to get oauth key", zap.Error(err)) + return nil, errors.E(op, err) + } + var pem []byte + err = json.Unmarshal(secret.Data, &pem) + if err != nil { + zapctx.Error(ctx, "failed to unmarshal pem data", zap.Error(err)) + return nil, errors.E(op, err) + } + return pem, nil +} + +// PutOAuthSecret puts a HS256 (symmetric encryption) secret into the credentials store for signing OAuth session tokens. +func (d *Database) PutOAuthSecret(ctx context.Context, raw []byte) error { + const op = errors.Op("database.PutOAuthSecret") + oauthKey, err := json.Marshal(raw) + if err != nil { + zapctx.Error(ctx, "failed to marshal pem data", zap.Error(err)) + return errors.E(op, err, "failed to marshal oauth key") + } + secret := dbmodel.NewSecret(oauthKind, oauthKeyTag, oauthKey) + return d.UpsertSecret(ctx, &secret) +} diff --git a/internal/db/secrets_test.go b/internal/db/secrets_test.go index 71e2249f4..cc3f79af3 100644 --- a/internal/db/secrets_test.go +++ b/internal/db/secrets_test.go @@ -279,3 +279,31 @@ func (s *dbSuite) TestCleanupJWKS(c *qt.C) { c.Assert(s.Database.DB.Model(&dbmodel.Secret{}).Count(&count).Error, qt.IsNil) c.Assert(count, qt.Equals, int64(0)) } + +func (s *dbSuite) TestPutAndGetOAuthSecret(c *qt.C) { + err := s.Database.Migrate(context.Background(), true) + c.Assert(err, qt.Equals, nil) + ctx := context.Background() + key := []byte(uuid.NewString()) + c.Assert(s.Database.PutOAuthSecret(ctx, key), qt.IsNil) + + secret := dbmodel.Secret{} + tx := s.Database.DB.First(&secret) + c.Assert(tx.Error, qt.IsNil) + c.Assert(secret.Type, qt.Equals, db.OAuthKind) + c.Assert(secret.Tag, qt.Equals, db.OAuthKeyTag) + + retrievedKey, err := s.Database.GetOAuthSecret(ctx) + c.Assert(err, qt.IsNil) + c.Assert(retrievedKey, qt.DeepEquals, key) +} + +func (s *dbSuite) TestGetOAuthSecretFailsIfNotFound(c *qt.C) { + err := s.Database.Migrate(context.Background(), true) + c.Assert(err, qt.Equals, nil) + ctx := context.Background() + + retrieved, err := s.Database.GetOAuthSecret(ctx) + c.Assert(err, qt.ErrorMatches, "secret not found") + c.Assert(retrieved, qt.IsNil) +} diff --git a/internal/jimm/cloudcredential_test.go b/internal/jimm/cloudcredential_test.go index 717a0fc04..2cd671d0c 100644 --- a/internal/jimm/cloudcredential_test.go +++ b/internal/jimm/cloudcredential_test.go @@ -1770,3 +1770,15 @@ func (s testCloudCredentialAttributeStore) PutJWKSExpiry(ctx context.Context, ex func (s testCloudCredentialAttributeStore) CleanupJWKS(ctx context.Context) error { return errors.E(errors.CodeNotImplemented) } + +func (s testCloudCredentialAttributeStore) CleanupOAuthSecrets(ctx context.Context) error { + return errors.E(errors.CodeNotImplemented) +} + +func (s testCloudCredentialAttributeStore) GetOAuthSecret(ctx context.Context) ([]byte, error) { + return nil, errors.E(errors.CodeNotImplemented) +} + +func (s testCloudCredentialAttributeStore) PutOAuthSecret(ctx context.Context, raw []byte) error { + return errors.E(errors.CodeNotImplemented) +} diff --git a/internal/jimm/credentials/credentials.go b/internal/jimm/credentials/credentials.go index 239854042..4f28d70ee 100644 --- a/internal/jimm/credentials/credentials.go +++ b/internal/jimm/credentials/credentials.go @@ -1,5 +1,7 @@ // Copyright 2023 canonical. +// Package credentials provides abstractions/definitions for credential storage +// backends and caching mechanisms. package credentials import ( @@ -16,6 +18,7 @@ import ( // - JWK Set // - JWK expiry // - JWK private key +// - OAuth session signing secret type CredentialStore interface { // Get retrieves the stored attributes of a cloud credential. Get(context.Context, names.CloudCredentialTag) (map[string]string, error) @@ -51,4 +54,13 @@ type CredentialStore interface { // PutJWKSExpiry sets the expiry time for the current JWKS within the store. PutJWKSExpiry(ctx context.Context, expiry time.Time) error + + // CleanupOAuthSecrets removes all secrets associated with OAuth. + CleanupOAuthSecrets(ctx context.Context) error + + // GetOAuthSecret returns the current HS256 (symmetric encryption) secret used to sign OAuth session tokens. + GetOAuthSecret(ctx context.Context) ([]byte, error) + + // PutOAuthSecret puts a HS256 (symmetric encryption) secret into the credentials store for signing OAuth session tokens. + PutOAuthSecret(ctx context.Context, raw []byte) error } diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index af6ce42f3..828e30109 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -173,6 +173,11 @@ type OAuthAuthenticator interface { VerifyClientCredentials(ctx context.Context, clientID string, clientSecret string) error } +// GetCredentialStore returns the credential store used by JIMM. +func (j *JIMM) GetCredentialStore() credentials.CredentialStore { + return j.CredentialStore +} + type permission struct { resource string relation string diff --git a/internal/jimm/jimm_test.go b/internal/jimm/jimm_test.go index 166cfae15..690d5410a 100644 --- a/internal/jimm/jimm_test.go +++ b/internal/jimm/jimm_test.go @@ -696,7 +696,7 @@ func TestFillMigrationTarget(t *testing.T) { err := db.Migrate(ctx, false) c.Assert(err, qt.IsNil) - store := &jimmtest.InMemoryCredentialStore{} + store := jimmtest.NewInMemoryCredentialStore() err = store.PutControllerCredentials(context.Background(), test.controllerName, "admin", "test-secret") c.Assert(err, qt.IsNil) @@ -775,7 +775,7 @@ func TestInitiateInternalMigration(t *testing.T) { c.Patch(jimm.InitiateMigration, func(ctx context.Context, j *jimm.JIMM, user *openfga.User, spec jujuparams.MigrationSpec, targetID uint) (jujuparams.InitiateMigrationResult, error) { return jujuparams.InitiateMigrationResult{}, nil }) - store := &jimmtest.InMemoryCredentialStore{} + store := jimmtest.NewInMemoryCredentialStore() err := store.PutControllerCredentials(context.Background(), test.migrateInfo.TargetController, "admin", "test-secret") c.Assert(err, qt.IsNil) diff --git a/internal/jimmhttp/auth_handler_test.go b/internal/jimmhttp/auth_handler_test.go index 849623985..81cdc95ef 100644 --- a/internal/jimmhttp/auth_handler_test.go +++ b/internal/jimmhttp/auth_handler_test.go @@ -4,14 +4,12 @@ import ( "context" "fmt" "io" - "math/rand" "net" "net/http" "net/http/cookiejar" "net/http/httptest" "net/url" "regexp" - "strconv" "testing" "time" @@ -42,17 +40,14 @@ func setupDbAndSessionStore(c *qt.C) (*db.Database, *pgstore.PGStore) { } func setupTestServer(c *qt.C, dashboardURL string, db *db.Database, sessionStore *pgstore.PGStore) *httptest.Server { + // Find a random free TCP port. + listener, err := net.Listen("tcp", "127.0.0.1:0") + c.Assert(err, qt.IsNil) + port := fmt.Sprintf("%d", listener.Addr().(*net.TCPAddr).Port) + // Create unstarted server to enable auth service s := httptest.NewUnstartedServer(nil) - // Setup random port listener - minPort := 30000 - maxPort := 50000 - - port := strconv.Itoa(rand.Intn(maxPort-minPort+1) + minPort) - l, err := net.Listen("tcp", "localhost:"+port) - c.Assert(err, qt.IsNil) - // Set the listener with a random port - s.Listener = l + s.Listener = listener // Remember redirect url to check it matches after test server starts redirectURL := "http://127.0.0.1:" + port + "/callback" diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index fe7066463..19212b0f1 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -18,8 +18,8 @@ import ( "github.com/canonical/jimm/internal/openfga" ) -var ( - jwtTestSecret = "test-secret" +const ( + JWTTestSecret = "test-secret" ) // A SimpleTester is a simple version of the test interface @@ -70,7 +70,7 @@ func NewUserSessionLogin(c SimpleTester, username string) api.LoginProvider { c.Fatalf("failed to generate test session token") } - freshToken, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte(jwtTestSecret))) + freshToken, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte(JWTTestSecret))) if err != nil { c.Fatalf("failed to sign test session token") } diff --git a/internal/jimmtest/jimm_mock.go b/internal/jimmtest/jimm_mock.go index b22deb2e6..6b598f344 100644 --- a/internal/jimmtest/jimm_mock.go +++ b/internal/jimmtest/jimm_mock.go @@ -17,6 +17,7 @@ import ( "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/jimm" + jimmcreds "github.com/canonical/jimm/internal/jimm/credentials" "github.com/canonical/jimm/internal/openfga" ofganames "github.com/canonical/jimm/internal/openfga/names" "github.com/canonical/jimm/internal/pubsub" @@ -59,6 +60,7 @@ type JIMM struct { GetCloudCredential_ func(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) GetCloudCredentialAttributes_ func(ctx context.Context, u *openfga.User, cred *dbmodel.CloudCredential, hidden bool) (attrs map[string]string, redacted []string, err error) GetControllerConfig_ func(ctx context.Context, u *dbmodel.Identity) (*dbmodel.ControllerConfig, error) + GetCredentialStore_ func() jimmcreds.CredentialStore GetJimmControllerAccess_ func(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error) GetUser_ func(ctx context.Context, username string) (*openfga.User, error) GetOpenFGAUserAndAuthorise_ func(ctx context.Context, email string) (*openfga.User, error) @@ -300,6 +302,12 @@ func (j *JIMM) GetControllerConfig(ctx context.Context, u *dbmodel.Identity) (*d } return j.GetControllerConfig_(ctx, u) } +func (j *JIMM) GetCredentialStore() jimmcreds.CredentialStore { + if j.GetCredentialStore_ == nil { + return nil + } + return j.GetCredentialStore_() +} func (j *JIMM) GetJimmControllerAccess(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error) { if j.GetJimmControllerAccess_ == nil { return "", errors.E(errors.CodeNotImplemented) diff --git a/internal/jimmtest/store.go b/internal/jimmtest/store.go index 9e542e6b6..f44fc0972 100644 --- a/internal/jimmtest/store.go +++ b/internal/jimmtest/store.go @@ -24,10 +24,19 @@ type InMemoryCredentialStore struct { jwks jwk.Set privateKey []byte expiry time.Time + oauthKey []byte controllerCredentials map[string]controllerCredentials cloudCredentialAttributes map[string]map[string]string } +// NewInMemoryCredentialStore returns a new instance of `InMemoryCredentialStore` +// with some secrets/keys being populated. +func NewInMemoryCredentialStore() *InMemoryCredentialStore { + return &InMemoryCredentialStore{ + oauthKey: []byte(JWTTestSecret), + } +} + // Get retrieves the stored attributes of a cloud credential. func (s *InMemoryCredentialStore) Get(ctx context.Context, credTag names.CloudCredentialTag) (map[string]string, error) { s.mu.Lock() @@ -177,3 +186,38 @@ func (s *InMemoryCredentialStore) PutJWKSExpiry(ctx context.Context, expiry time return nil } + +// CleanupOAuthSecrets removes all secrets associated with OAuth. +func (s *InMemoryCredentialStore) CleanupOAuthSecrets(ctx context.Context) error { + s.mu.Lock() + defer s.mu.Unlock() + + s.oauthKey = nil + return nil +} + +// GetOAuthSecret returns the current HS256 (symmetric encryption) secret used to sign OAuth session tokens. +func (s *InMemoryCredentialStore) GetOAuthSecret(ctx context.Context) ([]byte, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + if s.oauthKey == nil || len(s.oauthKey) == 0 { + return nil, errors.E(errors.CodeNotFound) + } + + key := make([]byte, len(s.oauthKey)) + copy(key, s.oauthKey) + + return key, nil +} + +// PutOAuthSecret puts a HS256 (symmetric encryption) secret into the credentials store for signing OAuth session tokens. +func (s *InMemoryCredentialStore) PutOAuthSecret(ctx context.Context, raw []byte) error { + s.mu.Lock() + defer s.mu.Unlock() + + s.oauthKey = make([]byte, len(raw)) + copy(s.oauthKey, raw) + + return nil +} diff --git a/internal/jimmtest/suite.go b/internal/jimmtest/suite.go index 01fc0d9a8..ff8a465e1 100644 --- a/internal/jimmtest/suite.go +++ b/internal/jimmtest/suite.go @@ -74,7 +74,7 @@ func (s *JIMMSuite) SetUpTest(c *gc.C) { Database: db.Database{ DB: PostgresDB(GocheckTester{c}, nil), }, - CredentialStore: &InMemoryCredentialStore{}, + CredentialStore: NewInMemoryCredentialStore(), Pubsub: &pubsub.Hub{MaxConcurrency: 10}, UUID: ControllerUUID, OpenFGAClient: s.OFGAClient, @@ -84,7 +84,7 @@ func (s *JIMMSuite) SetUpTest(c *gc.C) { s.cancel = cancel // Note that the secret key here must match what is used in tests. - s.JIMM.OAuthAuthenticator = NewMockOAuthAuthenticator(jwtTestSecret) + s.JIMM.OAuthAuthenticator = NewMockOAuthAuthenticator(JWTTestSecret) err = s.JIMM.Database.Migrate(ctx, false) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index 80868248b..e6e8644a5 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -88,9 +88,12 @@ func (r *controllerRoot) GetDeviceSessionToken(ctx context.Context) (params.GetD return response, errors.E(op, err) } - // TODO(ale8k): Add vault logic to get secret key and generate one - // on start up. - encToken, err := authSvc.MintSessionToken(email, "test-secret") + secretKey, err := r.jimm.GetCredentialStore().GetOAuthSecret(ctx) + if err != nil { + return response, errors.E(op, err, "failed to retrieve oauth secret key") + } + + encToken, err := authSvc.MintSessionToken(email, string(secretKey)) if err != nil { return response, errors.E(op, err) } @@ -110,9 +113,12 @@ func (r *controllerRoot) LoginWithSessionToken(ctx context.Context, req params.L authenticationSvc := r.jimm.OAuthAuthenticationService() // Verify the session token - // TODO(CSS-7081): Ensure for tests that the secret key can be configured. - // Or configure cmd tests to use the configured secret. - jwtToken, err := authenticationSvc.VerifySessionToken(req.SessionToken, "test-secret") + secretKey, err := r.jimm.GetCredentialStore().GetOAuthSecret(ctx) + if err != nil { + return jujuparams.LoginResult{}, errors.E(op, err, "failed to retrieve oauth secret key") + } + + jwtToken, err := authenticationSvc.VerifySessionToken(req.SessionToken, string(secretKey)) if err != nil { var aerr *auth.AuthenticationError if stderrors.As(err, &aerr) { diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index 883cd9881..8c3eb6778 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -19,6 +19,7 @@ import ( "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/jimm" + "github.com/canonical/jimm/internal/jimm/credentials" "github.com/canonical/jimm/internal/jujuapi/rpc" "github.com/canonical/jimm/internal/openfga" ofganames "github.com/canonical/jimm/internal/openfga/names" @@ -57,6 +58,7 @@ type JIMM interface { GetCloudCredential(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) GetCloudCredentialAttributes(ctx context.Context, u *openfga.User, cred *dbmodel.CloudCredential, hidden bool) (attrs map[string]string, redacted []string, err error) GetControllerConfig(ctx context.Context, u *dbmodel.Identity) (*dbmodel.ControllerConfig, error) + GetCredentialStore() credentials.CredentialStore GetJimmControllerAccess(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error) GetUser(ctx context.Context, username string) (*openfga.User, error) GetUserCloudAccess(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error) diff --git a/internal/vault/vault.go b/internal/vault/vault.go index a9e7c4cb0..a25809963 100644 --- a/internal/vault/vault.go +++ b/internal/vault/vault.go @@ -59,7 +59,7 @@ func (s *VaultStore) Get(ctx context.Context, tag names.CloudCredentialTag) (map return nil, errors.E(op, err) } - secret, err := client.Logical().Read(s.path(tag)) + secret, err := client.Logical().ReadWithContext(ctx, s.path(tag)) if err != nil { return nil, errors.E(op, err) } @@ -96,7 +96,7 @@ func (s *VaultStore) Put(ctx context.Context, tag names.CloudCredentialTag, attr for k, v := range attr { data[k] = v } - _, err = client.Logical().Write(s.path(tag), data) + _, err = client.Logical().WriteWithContext(ctx, s.path(tag), data) if err != nil { return errors.E(op, err) } @@ -112,7 +112,7 @@ func (s *VaultStore) delete(ctx context.Context, tag names.CloudCredentialTag) e if err != nil { return errors.E(op, err) } - _, err = client.Logical().Delete(s.path(tag)) + _, err = client.Logical().DeleteWithContext(ctx, s.path(tag)) if rerr, ok := err.(*api.ResponseError); ok && rerr.StatusCode == http.StatusNotFound { // Ignore the error if attempting to delete something that isn't there. err = nil @@ -133,7 +133,7 @@ func (s *VaultStore) GetControllerCredentials(ctx context.Context, controllerNam return "", "", errors.E(op, err) } - secret, err := client.Logical().Read(s.controllerCredentialsPath(controllerName)) + secret, err := client.Logical().ReadWithContext(ctx, s.controllerCredentialsPath(controllerName)) if err != nil { return "", "", errors.E(op, err) } @@ -169,7 +169,7 @@ func (s *VaultStore) PutControllerCredentials(ctx context.Context, controllerNam usernameKey: username, passwordKey: password, } - _, err = client.Logical().Write(s.controllerCredentialsPath(controllerName), data) + _, err = client.Logical().WriteWithContext(ctx, s.controllerCredentialsPath(controllerName), data) if err != nil { return errors.E(op, err) } @@ -186,9 +186,9 @@ func (s *VaultStore) CleanupJWKS(ctx context.Context) error { } // Vault does not return errors on deletion requests where // the secret does not exist. As such we just return the last known error. - client.Logical().Delete(s.getJWKSExpiryPath()) - client.Logical().Delete(s.getJWKSPath()) - if _, err = client.Logical().Delete(s.getJWKSPrivateKeyPath()); err != nil { + client.Logical().DeleteWithContext(ctx, s.getJWKSExpiryPath()) + client.Logical().DeleteWithContext(ctx, s.getJWKSPath()) + if _, err = client.Logical().DeleteWithContext(ctx, s.getJWKSPrivateKeyPath()); err != nil { return errors.E(op, err) } return nil @@ -203,7 +203,7 @@ func (s *VaultStore) GetJWKS(ctx context.Context) (jwk.Set, error) { return nil, errors.E(op, err) } - secret, err := client.Logical().Read(s.getJWKSPath()) + secret, err := client.Logical().ReadWithContext(ctx, s.getJWKSPath()) if err != nil { return nil, errors.E(op, err) } @@ -239,7 +239,7 @@ func (s *VaultStore) GetJWKSPrivateKey(ctx context.Context) ([]byte, error) { return nil, errors.E(op, err) } - secret, err := client.Logical().Read(s.getJWKSPrivateKeyPath()) + secret, err := client.Logical().ReadWithContext(ctx, s.getJWKSPrivateKeyPath()) if err != nil { return nil, errors.E(op, err) } @@ -269,7 +269,7 @@ func (s *VaultStore) GetJWKSExpiry(ctx context.Context) (time.Time, error) { return now, errors.E(op, err) } - secret, err := client.Logical().Read(s.getJWKSExpiryPath()) + secret, err := client.Logical().ReadWithContext(ctx, s.getJWKSExpiryPath()) if err != nil { return now, errors.E(op, err) } @@ -310,7 +310,8 @@ func (s *VaultStore) PutJWKS(ctx context.Context, jwks jwk.Set) error { return errors.E(op, err) } - _, err = client.Logical().WriteBytes( + _, err = client.Logical().WriteBytesWithContext( + ctx, // We persist in a similar folder to the controller credentials, but sub-route // to .well-known for further extensions and mental clarity within our vault. s.getJWKSPath(), @@ -332,7 +333,8 @@ func (s *VaultStore) PutJWKSPrivateKey(ctx context.Context, pem []byte) error { return errors.E(op, err) } - if _, err := client.Logical().Write( + if _, err := client.Logical().WriteWithContext( + ctx, // We persist in a similar folder to the controller credentials, but sub-route // to .well-known for further extensions and mental clarity within our vault. s.getJWKSPrivateKeyPath(), @@ -352,7 +354,8 @@ func (s *VaultStore) PutJWKSExpiry(ctx context.Context, expiry time.Time) error return errors.E(op, err) } - if _, err := client.Logical().Write( + if _, err := client.Logical().WriteWithContext( + ctx, s.getJWKSExpiryPath(), map[string]interface{}{ "jwks-expiry": expiry, @@ -385,6 +388,85 @@ func (s *VaultStore) getJWKSExpiryPath() string { return path.Join(s.getWellKnownPath(), "jwks-expiry") } +// CleanupOAuthSecrets removes all secrets associated with OAuth. +func (s *VaultStore) CleanupOAuthSecrets(ctx context.Context) error { + const op = errors.Op("vault.CleanupOAuthSecrets") + + client, err := s.client(ctx) + if err != nil { + return errors.E(op, err) + } + + // Vault does not return errors on deletion requests where + // the secret does not exist. + if _, err := client.Logical().DeleteWithContext(ctx, s.GetOAuthSecretPath()); err != nil { + return errors.E(op, err) + } + return nil +} + +// GetOAuthSecret returns the current HS256 (symmetric encryption) secret used to sign OAuth session tokens. +func (s *VaultStore) GetOAuthSecret(ctx context.Context) ([]byte, error) { + const op = errors.Op("vault.GetOAuthSecret") + + client, err := s.client(ctx) + if err != nil { + return nil, errors.E(op, err) + } + + secret, err := client.Logical().ReadWithContext(ctx, s.GetOAuthSecretPath()) + if err != nil { + return nil, errors.E(op, err) + } + + if secret == nil { + msg := "no OAuth key exists" + zapctx.Debug(ctx, msg) + return nil, errors.E(op, errors.CodeNotFound, msg) + } + + raw := secret.Data["key"] + if secret.Data["key"] == nil { + msg := "nil OAuth key data" + zapctx.Debug(ctx, msg) + return nil, errors.E(op, errors.CodeNotFound, msg) + } + + keyPemB64 := raw.(string) + + keyPem, err := base64.StdEncoding.DecodeString(keyPemB64) + if err != nil { + return nil, errors.E(op, err) + } + + return keyPem, nil +} + +// PutOAuthSecret puts a HS256 (symmetric encryption) secret into the credentials store for signing OAuth session tokens. +func (s *VaultStore) PutOAuthSecret(ctx context.Context, raw []byte) error { + const op = errors.Op("vault.PutOAuthSecret") + + client, err := s.client(ctx) + if err != nil { + return errors.E(op, err) + } + + if _, err := client.Logical().WriteWithContext( + ctx, + s.GetOAuthSecretPath(), + map[string]interface{}{"key": raw}, + ); err != nil { + return errors.E(op, err) + } + return nil +} + +// GetOAuthSecretPath returns a hardcoded suffixed vault path (dependent on +// the initial KVPath) to the OAuth JWK location. +func (s *VaultStore) GetOAuthSecretPath() string { + return path.Join(s.KVPath, "creds", "oauth", "key") +} + // deleteControllerCredentials removes the credentials associated with the controller in // the vault service. func (s *VaultStore) deleteControllerCredentials(ctx context.Context, controllerName string) error { @@ -394,7 +476,7 @@ func (s *VaultStore) deleteControllerCredentials(ctx context.Context, controller if err != nil { return errors.E(op, err) } - _, err = client.Logical().Delete(s.controllerCredentialsPath(controllerName)) + _, err = client.Logical().DeleteWithContext(ctx, s.controllerCredentialsPath(controllerName)) if rerr, ok := err.(*api.ResponseError); ok && rerr.StatusCode == http.StatusNotFound { // Ignore the error if attempting to delete something that isn't there. err = nil @@ -417,7 +499,7 @@ func (s *VaultStore) client(ctx context.Context) (*api.Client, error) { return s.client_, nil } - secret, err := s.Client.Logical().Write(s.AuthPath, s.AuthSecret) + secret, err := s.Client.Logical().WriteWithContext(ctx, s.AuthPath, s.AuthSecret) if err != nil { return nil, errors.E(op, err) } diff --git a/internal/vault/vault_test.go b/internal/vault/vault_test.go index ab2f182e0..5a2eaa3bd 100644 --- a/internal/vault/vault_test.go +++ b/internal/vault/vault_test.go @@ -184,3 +184,44 @@ func TestGetAndPutJWKSPrivateKey(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(string(keyPem), qt.Contains, "-----BEGIN RSA PRIVATE KEY-----") } + +func TestGetAndPutOAuthSecret(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + store := newStore(c) + + // We didn't use a pre-defined/constant key here because in that case we had + // to make sure there's nothing left from last test runs in Vault. + key := []byte(uuid.NewString()) // A random UUID as key + err := store.PutOAuthSecret(ctx, key) + c.Assert(err, qt.IsNil) + retrievedKey, err := store.GetOAuthSecret(ctx) + c.Assert(err, qt.IsNil) + c.Assert(retrievedKey, qt.DeepEquals, key) +} + +func TestGetOAuthSecretFailsIfDataIsNil(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + store := newStore(c) + + err := store.PutOAuthSecret(ctx, nil) + c.Assert(err, qt.IsNil) + + retrieved, err := store.GetOAuthSecret(ctx) + c.Assert(err, qt.ErrorMatches, "nil OAuth key data") + c.Assert(retrieved, qt.IsNil) +} + +func TestGetOAuthSecretFailsIfNotFound(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + store := newStore(c) + + err := store.CleanupOAuthSecrets(ctx) + c.Assert(err, qt.IsNil) + + retrieved, err := store.GetOAuthSecret(ctx) + c.Assert(err, qt.ErrorMatches, "no OAuth key exists") + c.Assert(retrieved, qt.IsNil) +} diff --git a/service.go b/service.go index a73b5505e..72936970c 100644 --- a/service.go +++ b/service.go @@ -4,6 +4,7 @@ package jimm import ( "context" + "crypto/rand" "database/sql" "net/http" "net/url" @@ -492,6 +493,34 @@ func newVaultStore(ctx context.Context, p Params) (jimmcreds.CredentialStore, er }, nil } +// CheckOrGenerateOAuthKey checks if the OAuth secret key already exists on the +// credential store, and if not, generates a random 4096-bit secret key and +func (s *Service) CheckOrGenerateOAuthKey(ctx context.Context) error { + const op = errors.Op("CheckOrGenerateOAuthKey") + store := s.jimm.CredentialStore + if store == nil { + zapctx.Info(ctx, "skipped generating initial OAuth secret key due to nil credential store") + return nil + } + + if secret, err := store.GetOAuthSecret(ctx); err == nil && secret != nil && len(secret) > 0 { + zapctx.Info(ctx, "detected existing OAuth secret key") + return nil + } + + secret := make([]byte, 4096) + if _, err := rand.Read(secret); err != nil { + zapctx.Error(ctx, "failed to generate OAuth secret key", zap.Error(err)) + return errors.E(op, err, "failed to generate OAuth secret key") + } + + if err := store.PutOAuthSecret(ctx, secret); err != nil { + zapctx.Error(ctx, "failed to store generated OAuth secret key", zap.Error(err)) + return errors.E(op, err, "failed to store generated OAuth secret key") + } + return nil +} + func newOpenFGAClient(ctx context.Context, p OpenFGAParams) (*openfga.OFGAClient, error) { const op = errors.Op("newOpenFGAClient") cofgaClient, err := cofga.NewClient(ctx, cofga.OpenFGAParams{ diff --git a/service_test.go b/service_test.go index 5d28b5801..17d61bef2 100644 --- a/service_test.go +++ b/service_test.go @@ -84,6 +84,7 @@ func TestServiceStartsWithoutSecretStore(t *testing.T) { func TestAuthenticator(t *testing.T) { c := qt.New(t) + ctx := context.Background() _, _, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) @@ -102,7 +103,10 @@ func TestAuthenticator(t *testing.T) { }, DashboardFinalRedirectURL: "", } - svc, err := jimm.NewService(context.Background(), p) + svc, err := jimm.NewService(ctx, p) + c.Assert(err, qt.IsNil) + + err = svc.JIMM().GetCredentialStore().PutOAuthSecret(ctx, []byte(jimmtest.JWTTestSecret)) c.Assert(err, qt.IsNil) srv := httptest.NewTLSServer(svc) @@ -151,6 +155,7 @@ const testVaultEnv = `clouds: func TestVault(t *testing.T) { c := qt.New(t) + ctx := context.Background() ofgaClient, _, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) @@ -173,7 +178,10 @@ func TestVault(t *testing.T) { } vaultClient, _, creds, _ := jimmtest.VaultClient(c, ".") - svc, err := jimm.NewService(context.Background(), p) + svc, err := jimm.NewService(ctx, p) + c.Assert(err, qt.IsNil) + + err = svc.JIMM().GetCredentialStore().PutOAuthSecret(ctx, []byte(jimmtest.JWTTestSecret)) c.Assert(err, qt.IsNil) env := jimmtest.ParseEnvironment(c, testVaultEnv) @@ -246,15 +254,17 @@ func TestPostgresSecretStore(t *testing.T) { func TestOpenFGA(t *testing.T) { c := qt.New(t) + ctx := context.Background() _, _, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) p := jimm.Params{ - ControllerUUID: "6acf4fd8-32d6-49ea-b4eb-dcb9d1590c11", - DSN: jimmtest.CreateEmptyDatabase(c), - OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), - ControllerAdmins: []string{"alice", "eve"}, + ControllerUUID: "6acf4fd8-32d6-49ea-b4eb-dcb9d1590c11", + DSN: jimmtest.CreateEmptyDatabase(c), + OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), + ControllerAdmins: []string{"alice", "eve"}, + InsecureSecretStorage: true, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ IssuerURL: "http://localhost:8082/realms/jimm", ClientID: "jimm-device", @@ -263,7 +273,11 @@ func TestOpenFGA(t *testing.T) { }, DashboardFinalRedirectURL: "", } - svc, err := jimm.NewService(context.Background(), p) + + svc, err := jimm.NewService(ctx, p) + c.Assert(err, qt.IsNil) + + err = svc.JIMM().GetCredentialStore().PutOAuthSecret(ctx, []byte(jimmtest.JWTTestSecret)) c.Assert(err, qt.IsNil) srv := httptest.NewTLSServer(svc) From b9cb2ed284110b4f5053f6f090b416003418ae76 Mon Sep 17 00:00:00 2001 From: ale8k Date: Tue, 19 Mar 2024 13:51:01 +0000 Subject: [PATCH 082/126] pr comments --- internal/auth/oauth2.go | 59 +++++++++++--------- internal/auth/oauth2_test.go | 1 - internal/jimmhttp/websocket.go | 73 +++++++++++++++--------- internal/jujuapi/admin_test.go | 89 ++++++++++++++++-------------- internal/jujuapi/controllerroot.go | 1 - 5 files changed, 131 insertions(+), 92 deletions(-) diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index b669ed35f..f5ae3025d 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -119,10 +119,6 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP return nil, errors.E(op, errors.CodeServerConfiguration, err, "failed to create oidc provider") } - if params.SessionCookieMaxAge == 0 { - return nil, errors.E(op, errors.CodeServerConfiguration, err, "session cookie max age not set") - } - if params.SessionTokenExpiry == 0 { return nil, errors.E(op, errors.CodeServerConfiguration, err, "session token expiry not set") } @@ -417,51 +413,44 @@ func (as *AuthenticationService) CreateBrowserSession( func (as *AuthenticationService) AuthenticateBrowserSession(ctx context.Context, w http.ResponseWriter, req *http.Request) (context.Context, error) { const op = errors.Op("auth.AuthenticationService.AuthenticateBrowserSession") - // Get the session for this cookie session, err := as.sessionStore.Get(req, SessionName) if err != nil { return ctx, errors.E(op, err, "failed to retrieve session") } - // Get the identity id (email) - identityId := session.Values[SessionIdentityKey] + identityId, ok := session.Values[SessionIdentityKey] + if !ok { + return ctx, errors.E(op, "session is missing identity key") + } - // Check the access token is ok err = as.validateAndUpdateAccessToken(ctx, identityId) - // If it's not ok, kill their session if err != nil { - session.Options.MaxAge = -1 - if err := session.Save(req, w); err != nil { + if err := as.deleteSession(session, w, req); err != nil { return ctx, errors.E(op, err) } return ctx, errors.E(op, err) } - // Otherwise update the context with the identity id + ctx = ContextWithSessionIdentity(ctx, identityId) - // Extend the session - session.Options.MaxAge = as.sessionCookieMaxAge - if err = session.Save(req, w); err != nil { + if err := as.extendSession(session, w, req); err != nil { return ctx, errors.E(op, err) } - // And give the context back with the identity id present return ctx, nil } -// validateAndUpdateAccessToken +// validateAndUpdateAccessToken validates the access tokens expiry, and if it cannot, then +// it attempts to refresh the access token. func (as *AuthenticationService) validateAndUpdateAccessToken(ctx context.Context, email any) error { const op = errors.Op("auth.AuthenticationService.validateAndUpdateAccessToken") - // Cast the email, it is any because we pass it through the context when authenticating - // with cookies and it makes sense to handle the casting here emailStr, ok := email.(string) if !ok { return errors.E(op, "failed to cast email") } - // Get identity db := as.db u := &dbmodel.Identity{ Name: emailStr, @@ -470,7 +459,6 @@ func (as *AuthenticationService) validateAndUpdateAccessToken(ctx context.Contex return errors.E(op, err) } - // Construct token t := &oauth2.Token{ AccessToken: u.AccessToken, RefreshToken: u.RefreshToken, @@ -478,17 +466,14 @@ func (as *AuthenticationService) validateAndUpdateAccessToken(ctx context.Contex TokenType: u.AccessTokenType, } - // Check its valid if t.Valid() { return nil } - // Attempt to update the identity with a new token if err := as.refreshIdentitiesToken(ctx, emailStr, t); err != nil { return errors.E(op, err) } - // All good! return nil } @@ -501,7 +486,7 @@ func (as *AuthenticationService) refreshIdentitiesToken(ctx context.Context, ema tSrc := as.oauthConfig.TokenSource(ctx, t) - // Get a new access and refresh token + // Get a new access and refresh token (token source only has Token()) newToken, err := tSrc.Token() if err != nil { return errors.E(op, err, "failed to refresh token") @@ -513,3 +498,27 @@ func (as *AuthenticationService) refreshIdentitiesToken(ctx context.Context, ema return nil } + +func (as *AuthenticationService) deleteSession(session *sessions.Session, w http.ResponseWriter, req *http.Request) error { + const op = errors.Op("auth.AuthenticationService.deleteSession") + + session.Options.MaxAge = 0 + + if err := session.Save(req, w); err != nil { + return errors.E(op, err) + } + + return nil +} + +func (as *AuthenticationService) extendSession(session *sessions.Session, w http.ResponseWriter, req *http.Request) error { + const op = errors.Op("auth.AuthenticationService.extendSession") + + session.Options.MaxAge = as.sessionCookieMaxAge + + if err := session.Save(req, w); err != nil { + return errors.E(op, err) + } + + return nil +} diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 835d99ec0..178a7d8e1 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -405,7 +405,6 @@ func TestAuthenticateBrowserSessionHandlesExpiredAccessTokens(t *testing.T) { // Check identity added identityId := auth.SessionIdentityFromContext(ctx) c.Assert(identityId, qt.Equals, "jimm-test@canonical.com") - // Check recorder has Set-Cookie // Get identity again with new access token expiry and access token err = db.GetIdentity(ctx, &u) diff --git a/internal/jimmhttp/websocket.go b/internal/jimmhttp/websocket.go index caec08be5..a5c7c166e 100644 --- a/internal/jimmhttp/websocket.go +++ b/internal/jimmhttp/websocket.go @@ -36,31 +36,14 @@ type WSHandler struct { func (h *WSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctx := req.Context() - // We perform cookie authentication at the HTTP layer instead of WS - // due to limitations of setting and retrieving cookies in the WS layer. - // - // If no cookie is present, we expect 1 of 3 scenarios: - // 1. It's a device session token login. - // 2. It's a client credential login. - // 3. It's an "expired" cookie login, and as such no cookie - // has been sent with the request. The handling of this is within - // LoginWithSessionCookie, in which, due to no identityId being present - // we know the cookie expired or a request with no cookie was made. - _, err := req.Cookie(auth.SessionName) - - // Now we know a cookie is present, so let's try perform a cookie login / logic - // as presumably a cookie of this name should only ever be present in the case - // the browser performs a connection. - if err == nil { - ctx, err = h.Server.GetAuthenticationService().AuthenticateBrowserSession( - ctx, w, req, - ) - if err != nil { - // Something went wrong when trying to perform the authentication - // of the cookie. - w.WriteHeader(http.StatusInternalServerError) - return - } + ctx, err := handleBrowserAuthentication( + ctx, + h.Server.GetAuthenticationService(), + w, + req, + ) + if err != nil { + return } ctx = context.WithValue(ctx, contextPathKey("path"), req.URL.EscapedPath()) @@ -93,6 +76,46 @@ func (h *WSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { h.Server.ServeWS(ctx, conn) } +// handleBrowserAuthentication handles browser authentication when a session cookie +// is present, ultimately placing the identity resolved from the cookie within the +// passed context. +// +// It updates the response header on authentication errors with a InternalServerError, +// and as such is safe to return from your handler upon error without updating +// the response statuses. +func handleBrowserAuthentication(ctx context.Context, authSvc jimm.OAuthAuthenticator, w http.ResponseWriter, req *http.Request) (context.Context, error) { + // We perform cookie authentication at the HTTP layer instead of WS + // due to limitations of setting and retrieving cookies in the WS layer. + // + // If no cookie is present, we expect 1 of 3 scenarios: + // 1. It's a device session token login. + // 2. It's a client credential login. + // 3. It's an "expired" cookie login, and as such no cookie + // has been sent with the request. The handling of this is within + // LoginWithSessionCookie, in which, due to no identityId being present + // we know the cookie expired or a request with no cookie was made. + _, err := req.Cookie(auth.SessionName) + + // Now we know a cookie is present, so let's try perform a cookie login / logic + // as presumably a cookie of this name should only ever be present in the case + // the browser performs a connection. + if err == nil { + ctx, err = authSvc.AuthenticateBrowserSession( + ctx, w, req, + ) + if err != nil { + // Something went wrong when trying to perform the authentication + // of the cookie. + w.WriteHeader(http.StatusInternalServerError) + return ctx, err + } + } + + // If there's an error due to failure to find the cookie, just return the context + // and move on presuming it's a device or client credentials login. + return ctx, nil +} + // A WSServer is a websocket server. // // ServeWS should handle all messaging on the websocket connection and diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index 72b60b116..eaa33e82d 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -113,52 +113,13 @@ func (s *adminSuite) TestBrowserLogin(c *gc.C) { c.Assert(err, gc.IsNil) jar.SetCookies(jimmURL, cookies) - // Copied from github.com/juju/juju@v0.0.0-20240304110523-55fb5d03683b/api/apiclient.go - dialWebsocket := func(ctx context.Context, urlStr string, tlsConfig *tls.Config, ipAddr string) (jsoncodec.JSONConn, error) { - url, err := url.Parse(urlStr) - if err != nil { - return nil, errors.Trace(err) - } - - netDialer := net.Dialer{} - dialer := &websocket.Dialer{ - NetDial: func(netw, addr string) (net.Conn, error) { - if addr == url.Host { - addr = ipAddr - } - return netDialer.DialContext(ctx, netw, addr) - }, - Proxy: proxy.DefaultConfig.GetProxy, - HandshakeTimeout: 45 * time.Second, - TLSClientConfig: tlsConfig, - Jar: jar, - } - - c, resp, err := dialer.Dial(urlStr, nil) - if err != nil { - if err == websocket.ErrBadHandshake { - defer resp.Body.Close() - body, readErr := io.ReadAll(resp.Body) - if readErr == nil { - err = errors.Errorf( - "%s (%s)", - strings.TrimSpace(string(body)), - http.StatusText(resp.StatusCode), - ) - } - } - return nil, errors.Trace(err) - } - return jsoncodec.NewWebsocketConn(c), nil - } - conn := s.openWithDialWebsocket( c, &api.Info{ SkipLogin: true, }, "test", - dialWebsocket, + getDialWebsocketWithCustomCookieJar(jar), ) defer conn.Close() @@ -363,3 +324,51 @@ func (s *adminSuite) TestLoginWithClientCredentials(c *gc.C) { }, &loginResult) c.Assert(err, gc.ErrorMatches, `invalid client credentials \(unauthorized access\)`) } + +// getDialWebsocketWithCustomCookieJar is mostly the default dialer configuration exception +// we need a dial websocket for juju containing a custom cookie jar to send cookies to +// a new server url when testing LoginWithSessionCookie. As such this closure simply +// passes the jar through. +func getDialWebsocketWithCustomCookieJar(jar *cookiejar.Jar) func(ctx context.Context, urlStr string, tlsConfig *tls.Config, ipAddr string) (jsoncodec.JSONConn, error) { + // Copied from github.com/juju/juju@v0.0.0-20240304110523-55fb5d03683b/api/apiclient.go + dialWebsocket := func(ctx context.Context, urlStr string, tlsConfig *tls.Config, ipAddr string) (jsoncodec.JSONConn, error) { + url, err := url.Parse(urlStr) + if err != nil { + return nil, errors.Trace(err) + } + + netDialer := net.Dialer{} + dialer := &websocket.Dialer{ + NetDial: func(netw, addr string) (net.Conn, error) { + if addr == url.Host { + addr = ipAddr + } + return netDialer.DialContext(ctx, netw, addr) + }, + Proxy: proxy.DefaultConfig.GetProxy, + HandshakeTimeout: 45 * time.Second, + TLSClientConfig: tlsConfig, + // We update the jar so that the cookies retrieved from RunBrowserLogin + // can be sent in the LoginWithSessionCookie call. + Jar: jar, + } + + c, resp, err := dialer.Dial(urlStr, nil) + if err != nil { + if err == websocket.ErrBadHandshake { + defer resp.Body.Close() + body, readErr := io.ReadAll(resp.Body) + if readErr == nil { + err = errors.Errorf( + "%s (%s)", + strings.TrimSpace(string(body)), + http.StatusText(resp.StatusCode), + ) + } + } + return nil, errors.Trace(err) + } + return jsoncodec.NewWebsocketConn(c), nil + } + return dialWebsocket +} diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index 39661bcf5..14d360494 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -144,7 +144,6 @@ func newControllerRoot(j JIMM, p Params, identityId string) *controllerRoot { watchers: watcherRegistry, pingF: func() {}, controllerUUIDMasking: true, - user: nil, // TODO identityId: identityId, } From e9475ae2b4bbbfaba8c66862c24699cde569451e Mon Sep 17 00:00:00 2001 From: ale8k Date: Tue, 19 Mar 2024 13:55:27 +0000 Subject: [PATCH 083/126] Babaks changes for finding random tcp port --- internal/jimmtest/auth.go | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index 70aafb05b..357442bc6 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -8,14 +8,12 @@ import ( "errors" "fmt" "io" - "math/rand" "net" "net/http" "net/http/cookiejar" "net/http/httptest" "net/url" "regexp" - "strconv" "strings" "time" @@ -103,20 +101,16 @@ func convertUsernameToEmail(username string) string { } func SetupTestDashboardCallbackHandler(browserURL string, db *db.Database, sessionStore sessions.Store) (*httptest.Server, error) { - // Create unstarted server to enable auth service - s := httptest.NewUnstartedServer(nil) - // Setup random port listener - minPort := 30000 - maxPort := 50000 - - port := strconv.Itoa(rand.Intn(maxPort-minPort+1) + minPort) - l, err := net.Listen("tcp", "localhost:"+port) + // Find a random free TCP port. + listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, err } + port := fmt.Sprintf("%d", listener.Addr().(*net.TCPAddr).Port) - // Set the listener with a random port - s.Listener = l + // Create unstarted server to enable auth service + s := httptest.NewUnstartedServer(nil) + s.Listener = listener // Remember redirect url to check it matches after test server starts redirectURL := "http://127.0.0.1:" + port + "/callback" From 4c07c2a0426fd873fa5f36ccd2532d020a1f76a3 Mon Sep 17 00:00:00 2001 From: ale8k Date: Tue, 19 Mar 2024 14:54:13 +0000 Subject: [PATCH 084/126] Switch back to -1 --- internal/auth/oauth2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index f5ae3025d..e0465b124 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -502,7 +502,7 @@ func (as *AuthenticationService) refreshIdentitiesToken(ctx context.Context, ema func (as *AuthenticationService) deleteSession(session *sessions.Session, w http.ResponseWriter, req *http.Request) error { const op = errors.Op("auth.AuthenticationService.deleteSession") - session.Options.MaxAge = 0 + session.Options.MaxAge = -1 if err := session.Save(req, w); err != nil { return errors.E(op, err) From 6c80accabdccd9cb9248531240706421725316e8 Mon Sep 17 00:00:00 2001 From: alesstimec Date: Fri, 15 Mar 2024 17:31:46 +0100 Subject: [PATCH 085/126] Adds new login method handling to the model proxy. --- internal/jimm/access.go | 19 +- internal/jimm/access_test.go | 62 ++--- internal/jimm/admin.go | 67 +++++ internal/jimm/jimm.go | 7 - internal/jimmtest/gorm.go | 1 + internal/jujuapi/admin.go | 42 +-- internal/jujuapi/admin_test.go | 2 +- internal/jujuapi/controllerroot.go | 4 +- internal/jujuapi/websocket.go | 9 +- internal/rpc/client_test.go | 10 +- internal/rpc/proxy.go | 352 ++++++++++++++++-------- internal/rpc/proxy_test.go | 428 +++++++++++++++++++++++++++++ 12 files changed, 787 insertions(+), 216 deletions(-) create mode 100644 internal/jimm/admin.go create mode 100644 internal/rpc/proxy_test.go diff --git a/internal/jimm/access.go b/internal/jimm/access.go index da937474e..eb7268f38 100644 --- a/internal/jimm/access.go +++ b/internal/jimm/access.go @@ -176,7 +176,6 @@ type JWTService interface { // JWTGenerator provides the necessary state and methods to authorize a user and generate JWT tokens. type JWTGenerator struct { - authenticator Authenticator database JWTGeneratorDatabase accessChecker JWTGeneratorAccessChecker jwtService JWTService @@ -190,9 +189,8 @@ type JWTGenerator struct { } // NewJWTGenerator returns a new JwtAuthorizer struct -func NewJWTGenerator(authenticator Authenticator, database JWTGeneratorDatabase, accessChecker JWTGeneratorAccessChecker, jwtService JWTService) JWTGenerator { +func NewJWTGenerator(database JWTGeneratorDatabase, accessChecker JWTGeneratorAccessChecker, jwtService JWTService) JWTGenerator { return JWTGenerator{ - authenticator: authenticator, database: database, accessChecker: accessChecker, jwtService: jwtService, @@ -216,24 +214,21 @@ func (auth *JWTGenerator) GetUser() names.UserTag { // MakeLoginToken authorizes the user based on the provided login requests and returns // a JWT containing claims about user's access to the controller, model (if applicable) // and all clouds that the controller knows about. -func (auth *JWTGenerator) MakeLoginToken(ctx context.Context, req *jujuparams.LoginRequest) ([]byte, error) { +func (auth *JWTGenerator) MakeLoginToken(ctx context.Context, user *openfga.User) ([]byte, error) { const op = errors.Op("jimm.MakeLoginToken") auth.mu.Lock() defer auth.mu.Unlock() - if req == nil { - return nil, errors.E(op, "missing login request.") + if user == nil { + return nil, errors.E(op, "user not specified") } + auth.user = user + // Recreate the accessMapCache to prevent leaking permissions across multiple login requests. auth.accessMapCache = make(map[string]string) var authErr error - // TODO(CSS-7331) Refactor model proxy for new login methods - auth.user, authErr = auth.authenticator.Authenticate(ctx, req) - if authErr != nil { - zapctx.Error(ctx, "authentication failed", zap.Error(authErr)) - return nil, authErr - } + var modelAccess string if auth.mt.Id() == "" { return nil, errors.E(op, "model not set") diff --git a/internal/jimm/access_test.go b/internal/jimm/access_test.go index f595f40b5..24ac7b061 100644 --- a/internal/jimm/access_test.go +++ b/internal/jimm/access_test.go @@ -195,17 +195,15 @@ func TestJWTGeneratorMakeLoginToken(t *testing.T) { tests := []struct { about string - authenticator *testAuthenticator + username string database *testDatabase accessChecker *testAccessChecker jwtService *testJWTService expectedError string expectedJWTParams jimmjwx.JWTParams }{{ - about: "initial login, all is well", - authenticator: &testAuthenticator{ - username: "eve@canonical.com", - }, + about: "initial login, all is well", + username: "eve@canonical.com", database: &testDatabase{ ctl: dbmodel.Controller{ CloudRegions: []dbmodel.CloudRegionControllerPriority{{ @@ -239,27 +237,16 @@ func TestJWTGeneratorMakeLoginToken(t *testing.T) { }, }, }, { - about: "authorization fails", - authenticator: &testAuthenticator{ - username: "eve@canonical.com", - err: errors.E("a test error"), - }, - expectedError: "a test error", - }, { - about: "model access check fails", - authenticator: &testAuthenticator{ - username: "eve@canonical.com", - }, + about: "model access check fails", + username: "eve@canonical.com", accessChecker: &testAccessChecker{ modelAccessCheckErr: errors.E("a test error"), }, jwtService: &testJWTService{}, expectedError: "a test error", }, { - about: "controller access check fails", - authenticator: &testAuthenticator{ - username: "eve@canonical.com", - }, + about: "controller access check fails", + username: "eve@canonical.com", accessChecker: &testAccessChecker{ modelAccess: map[string]string{ mt.String(): "admin", @@ -268,10 +255,8 @@ func TestJWTGeneratorMakeLoginToken(t *testing.T) { }, expectedError: "a test error", }, { - about: "get controller from db fails", - authenticator: &testAuthenticator{ - username: "eve@canonical.com", - }, + about: "get controller from db fails", + username: "eve@canonical.com", database: &testDatabase{ err: errors.E("a test error"), }, @@ -285,10 +270,8 @@ func TestJWTGeneratorMakeLoginToken(t *testing.T) { }, expectedError: "failed to fetch controller", }, { - about: "cloud access check fails", - authenticator: &testAuthenticator{ - username: "eve@canonical.com", - }, + about: "cloud access check fails", + username: "eve@canonical.com", database: &testDatabase{ ctl: dbmodel.Controller{ CloudRegions: []dbmodel.CloudRegionControllerPriority{{ @@ -311,10 +294,8 @@ func TestJWTGeneratorMakeLoginToken(t *testing.T) { }, expectedError: "failed to check user's cloud access", }, { - about: "jwt service errors out", - authenticator: &testAuthenticator{ - username: "eve@canonical.com", - }, + about: "jwt service errors out", + username: "eve@canonical.com", database: &testDatabase{ ctl: dbmodel.Controller{ CloudRegions: []dbmodel.CloudRegionControllerPriority{{ @@ -344,10 +325,14 @@ func TestJWTGeneratorMakeLoginToken(t *testing.T) { }} for _, test := range tests { - generator := jimm.NewJWTGenerator(test.authenticator, test.database, test.accessChecker, test.jwtService) + generator := jimm.NewJWTGenerator(test.database, test.accessChecker, test.jwtService) generator.SetTags(mt, ct) - _, err := generator.MakeLoginToken(context.Background(), &jujuparams.LoginRequest{}) + _, err := generator.MakeLoginToken(context.Background(), &openfga.User{ + Identity: &dbmodel.Identity{ + Name: test.username, + }, + }) if test.expectedError != "" { c.Assert(err, qt.ErrorMatches, test.expectedError) } else { @@ -414,9 +399,6 @@ func TestJWTGeneratorMakeToken(t *testing.T) { for _, test := range tests { generator := jimm.NewJWTGenerator( - &testAuthenticator{ - username: "eve@canonical.com", - }, &testDatabase{ ctl: dbmodel.Controller{ CloudRegions: []dbmodel.CloudRegionControllerPriority{{ @@ -445,7 +427,11 @@ func TestJWTGeneratorMakeToken(t *testing.T) { ) generator.SetTags(mt, ct) - _, err := generator.MakeLoginToken(context.Background(), &jujuparams.LoginRequest{}) + _, err := generator.MakeLoginToken(context.Background(), &openfga.User{ + Identity: &dbmodel.Identity{ + Name: "eve@canonical.com", + }, + }) c.Assert(err, qt.IsNil) _, err = generator.MakeToken(context.Background(), test.permissions) diff --git a/internal/jimm/admin.go b/internal/jimm/admin.go new file mode 100644 index 000000000..d39f97b1a --- /dev/null +++ b/internal/jimm/admin.go @@ -0,0 +1,67 @@ +// Copyright 2024 Canonical Ltd. + +package jimm + +import ( + "context" + + "golang.org/x/oauth2" + + "github.com/canonical/jimm/internal/errors" + "github.com/canonical/jimm/internal/jimm/credentials" +) + +// LoginDevice starts the device login flow. +func LoginDevice(ctx context.Context, authenticator OAuthAuthenticator) (*oauth2.DeviceAuthResponse, error) { + const op = errors.Op("jujuapi.LoginDevice") + + deviceResponse, err := authenticator.Device(ctx) + if err != nil { + return nil, errors.E(op, err) + } + + return deviceResponse, nil +} + +func GetDeviceSessionToken(ctx context.Context, authenticator OAuthAuthenticator, credentialStore credentials.CredentialStore, deviceOAuthResponse *oauth2.DeviceAuthResponse) (string, error) { + const op = errors.Op("jujuapi.GetDeviceSessionToken") + + if authenticator == nil { + return "", errors.E("nil authenticator") + } + + if credentialStore == nil { + return "", errors.E("nil credential store") + } + + token, err := authenticator.DeviceAccessToken(ctx, deviceOAuthResponse) + if err != nil { + return "", errors.E(op, err) + } + + idToken, err := authenticator.ExtractAndVerifyIDToken(ctx, token) + if err != nil { + return "", errors.E(op, err) + } + + email, err := authenticator.Email(idToken) + if err != nil { + return "", errors.E(op, err) + } + + if err := authenticator.UpdateIdentity(ctx, email, token); err != nil { + return "", errors.E(op, err) + } + + secretKey, err := credentialStore.GetOAuthSecret(ctx) + if err != nil { + return "", errors.E(op, err, "failed to retrieve oauth secret key") + } + + encToken, err := authenticator.MintSessionToken(email, string(secretKey)) + if err != nil { + return "", errors.E(op, err) + } + + return string(encToken), nil +} diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index 828e30109..388690fd1 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -115,13 +115,6 @@ func (j *JIMM) AuthorizationClient() *openfga.OFGAClient { return j.OpenFGAClient } -// An Authenticator authenticates login requests. -type Authenticator interface { - // Authenticate processes the given LoginRequest and returns the user - // that has authenticated. - Authenticate(ctx context.Context, req *jujuparams.LoginRequest) (*openfga.User, error) -} - // OAuthAuthenticator is responsible for handling authentication // via OAuth2.0 AND JWT access tokens to JIMM. type OAuthAuthenticator interface { diff --git a/internal/jimmtest/gorm.go b/internal/jimmtest/gorm.go index 3e8d9b1df..f7a214f56 100644 --- a/internal/jimmtest/gorm.go +++ b/internal/jimmtest/gorm.go @@ -110,6 +110,7 @@ func PostgresDB(t Tester, nowFunc func() time.Time) *gorm.DB { } suggestedName := "jimm_test_" + t.Name() + t.Logf("suggested db name: %s", suggestedName) _, dsn, err := createDatabaseFromTemplate(suggestedName, templateDatabaseName) if err != nil { t.Fatalf("error creating database (%s): %s", suggestedName, err) diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index e6e8644a5..e72965137 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -14,6 +14,7 @@ import ( "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/errors" + "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/openfga" ) @@ -28,12 +29,6 @@ func unsupportedLogin() error { var facadeInit = make(map[string]func(r *controllerRoot) []int) -// Login implements the Login method on the Admin facade. -func (r *controllerRoot) Login(ctx context.Context, req jujuparams.LoginRequest) (jujuparams.LoginResult, error) { - const op = errors.Op("jujuapi.Login") - return jujuparams.LoginResult{}, errors.E(op, "Invalid login, ensure you are using Juju 3.5+") -} - // LoginDevice starts a device login flow (typically a CLI). It will return a verification URI // and user code that the user is expected to enter into the verification URI link. // @@ -42,9 +37,8 @@ func (r *controllerRoot) Login(ctx context.Context, req jujuparams.LoginRequest) func (r *controllerRoot) LoginDevice(ctx context.Context) (params.LoginDeviceResponse, error) { const op = errors.Op("jujuapi.LoginDevice") response := params.LoginDeviceResponse{} - authSvc := r.jimm.OAuthAuthenticationService() - deviceResponse, err := authSvc.Device(ctx) + deviceResponse, err := jimm.LoginDevice(ctx, r.jimm.OAuthAuthenticationService()) if err != nil { return response, errors.E(op, err) } @@ -53,8 +47,8 @@ func (r *controllerRoot) LoginDevice(ctx context.Context) (params.LoginDeviceRes // happens on the SAME websocket. r.deviceOAuthResponse = deviceResponse - response.VerificationURI = deviceResponse.VerificationURI response.UserCode = deviceResponse.UserCode + response.VerificationURI = deviceResponse.VerificationURI return response, nil } @@ -67,39 +61,13 @@ func (r *controllerRoot) LoginDevice(ctx context.Context) (params.LoginDeviceRes func (r *controllerRoot) GetDeviceSessionToken(ctx context.Context) (params.GetDeviceSessionTokenResponse, error) { const op = errors.Op("jujuapi.GetDeviceSessionToken") response := params.GetDeviceSessionTokenResponse{} - authSvc := r.jimm.OAuthAuthenticationService() - - token, err := authSvc.DeviceAccessToken(ctx, r.deviceOAuthResponse) - if err != nil { - return response, errors.E(op, err) - } - - idToken, err := authSvc.ExtractAndVerifyIDToken(ctx, token) - if err != nil { - return response, errors.E(op, err) - } - email, err := authSvc.Email(idToken) + token, err := jimm.GetDeviceSessionToken(ctx, r.jimm.OAuthAuthenticationService(), r.jimm.GetCredentialStore(), r.deviceOAuthResponse) if err != nil { return response, errors.E(op, err) } - if err := authSvc.UpdateIdentity(ctx, email, token); err != nil { - return response, errors.E(op, err) - } - - secretKey, err := r.jimm.GetCredentialStore().GetOAuthSecret(ctx) - if err != nil { - return response, errors.E(op, err, "failed to retrieve oauth secret key") - } - - encToken, err := authSvc.MintSessionToken(email, string(secretKey)) - if err != nil { - return response, errors.E(op, err) - } - - response.SessionToken = string(encToken) - + response.SessionToken = token return response, nil } diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index 4bce1d761..6a704f4ce 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -56,7 +56,7 @@ func (s *adminSuite) TestLoginToController(c *gc.C) { }, "test") defer conn.Close() err := conn.Login(nil, "", "", nil) - c.Assert(err, gc.ErrorMatches, "Invalid login, ensure you are using Juju 3\\.5\\+") + c.Assert(err, gc.ErrorMatches, `JIMM does not support login from old clients \(not supported\)`) var resp jujuparams.RedirectInfoResult err = conn.APICall("Admin", 3, "", "RedirectInfo", nil, &resp) c.Assert(jujuparams.ErrCode(err), gc.Equals, jujuparams.CodeNotImplemented) diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index 8c3eb6778..9bcd708b9 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -147,8 +147,8 @@ func newControllerRoot(j JIMM, p Params) *controllerRoot { r.AddMethod("Admin", 1, "Login", rpc.Method(unsupportedLogin)) r.AddMethod("Admin", 2, "Login", rpc.Method(unsupportedLogin)) - r.AddMethod("Admin", 3, "Login", rpc.Method(r.Login)) - r.AddMethod("Admin", 4, "Login", rpc.Method(r.Login)) + r.AddMethod("Admin", 3, "Login", rpc.Method(unsupportedLogin)) + r.AddMethod("Admin", 4, "Login", rpc.Method(unsupportedLogin)) r.AddMethod("Admin", 4, "LoginDevice", rpc.Method(r.LoginDevice)) r.AddMethod("Admin", 4, "GetDeviceSessionToken", rpc.Method(r.GetDeviceSessionToken)) r.AddMethod("Admin", 4, "LoginWithSessionToken", rpc.Method(r.LoginWithSessionToken)) diff --git a/internal/jujuapi/websocket.go b/internal/jujuapi/websocket.go index 0d92260a5..fd2fc9a84 100644 --- a/internal/jujuapi/websocket.go +++ b/internal/jujuapi/websocket.go @@ -130,8 +130,7 @@ func modelInfoFromPath(path string) (uuid string, finalPath string, err error) { // ServeWS implements jimmhttp.WSServer. func (s modelProxyServer) ServeWS(ctx context.Context, clientConn *websocket.Conn) { - // TODO(CSS-7331) Refactor model proxy for new login methods - jwtGenerator := jimm.NewJWTGenerator(nil, &s.jimm.Database, s.jimm, s.jimm.JWTService) + jwtGenerator := jimm.NewJWTGenerator(&s.jimm.Database, s.jimm, s.jimm.JWTService) connectionFunc := controllerConnectionFunc(s, &jwtGenerator) zapctx.Debug(ctx, "Starting proxier") auditLogger := s.jimm.AddAuditLogEntry @@ -140,14 +139,15 @@ func (s modelProxyServer) ServeWS(ctx context.Context, clientConn *websocket.Con TokenGen: &jwtGenerator, ConnectController: connectionFunc, AuditLog: auditLogger, + JIMM: s.jimm, } jimmRPC.ProxySockets(ctx, proxyHelpers) } // controllerConnectionFunc returns a function that will be used to // connect to a controller when a client makes a request. -func controllerConnectionFunc(s modelProxyServer, jwtGenerator *jimm.JWTGenerator) func(context.Context) (*websocket.Conn, string, error) { - connectToControllerFunc := func(ctx context.Context) (*websocket.Conn, string, error) { +func controllerConnectionFunc(s modelProxyServer, jwtGenerator *jimm.JWTGenerator) func(context.Context) (jimmRPC.WebsocketConnection, string, error) { + return func(ctx context.Context) (jimmRPC.WebsocketConnection, string, error) { const op = errors.Op("proxy.controllerConnectionFunc") path := jimmhttp.PathElementFromContext(ctx, "path") zapctx.Debug(ctx, "grabbing model info from path", zap.String("path", path)) @@ -177,7 +177,6 @@ func controllerConnectionFunc(s modelProxyServer, jwtGenerator *jimm.JWTGenerato fullModelName := m.Controller.Name + "/" + m.Name return controllerConn, fullModelName, nil } - return connectToControllerFunc } // Use a 64k frame size for the websockets while we need to deal diff --git a/internal/rpc/client_test.go b/internal/rpc/client_test.go index 063c9b8df..e284853c0 100644 --- a/internal/rpc/client_test.go +++ b/internal/rpc/client_test.go @@ -15,11 +15,11 @@ import ( qt "github.com/frankban/quicktest" "github.com/gorilla/websocket" - "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" + "github.com/canonical/jimm/internal/openfga" "github.com/canonical/jimm/internal/rpc" ) @@ -225,7 +225,7 @@ func TestClientReceiveInvalidMessage(t *testing.T) { type testTokenGenerator struct{} -func (p *testTokenGenerator) MakeLoginToken(ctx context.Context, req *params.LoginRequest) ([]byte, error) { +func (p *testTokenGenerator) MakeLoginToken(ctx context.Context, user *openfga.User) ([]byte, error) { return nil, nil } @@ -250,7 +250,7 @@ func TestProxySockets(t *testing.T) { errChan := make(chan error) srvJIMM := newServer(func(connClient *websocket.Conn) error { testTokenGen := testTokenGenerator{} - f := func(context.Context) (*websocket.Conn, string, error) { + f := func(context.Context) (rpc.WebsocketConnection, string, error) { connController, err := srvController.dialer.DialWebsocket(ctx, srvController.URL) c.Assert(err, qt.IsNil) return connController, "TestName", nil @@ -297,7 +297,7 @@ func TestCancelProxySockets(t *testing.T) { errChan := make(chan error) srvJIMM := newServer(func(connClient *websocket.Conn) error { testTokenGen := testTokenGenerator{} - f := func(context.Context) (*websocket.Conn, string, error) { + f := func(context.Context) (rpc.WebsocketConnection, string, error) { connController, err := srvController.dialer.DialWebsocket(ctx, srvController.URL) c.Assert(err, qt.IsNil) return connController, "TestName", nil @@ -337,7 +337,7 @@ func TestProxySocketsAuditLogs(t *testing.T) { errChan := make(chan error) srvJIMM := newServer(func(connClient *websocket.Conn) error { testTokenGen := testTokenGenerator{} - f := func(context.Context) (*websocket.Conn, string, error) { + f := func(context.Context) (rpc.WebsocketConnection, string, error) { connController, err := srvController.dialer.DialWebsocket(ctx, srvController.URL) c.Assert(err, qt.IsNil) return connController, "TestModelName", nil diff --git a/internal/rpc/proxy.go b/internal/rpc/proxy.go index 9b208e7bc..b08a640b3 100644 --- a/internal/rpc/proxy.go +++ b/internal/rpc/proxy.go @@ -4,19 +4,21 @@ import ( "context" "encoding/base64" "encoding/json" - stderrors "errors" "sync" "time" - "github.com/gorilla/websocket" "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" + "golang.org/x/oauth2" - "github.com/canonical/jimm/internal/auth" + apiparams "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" + "github.com/canonical/jimm/internal/jimm" + "github.com/canonical/jimm/internal/jimm/credentials" + "github.com/canonical/jimm/internal/openfga" "github.com/canonical/jimm/internal/utils" ) @@ -26,10 +28,10 @@ const ( // TokenGenerator authenticates a user and generates a JWT token. type TokenGenerator interface { - // MakeLoginToken authorizes the user based on the provided login requests and returns - // a JWT containing claims about user's access to the controller, model (if applicable) - // and all clouds that the controller knows about. - MakeLoginToken(ctx context.Context, req *params.LoginRequest) ([]byte, error) + // MakeLoginToken returns a JWT containing claims about user's access + // to the controller, model (if applicable) and all clouds that the + // controller knows about. + MakeLoginToken(ctx context.Context, user *openfga.User) ([]byte, error) // MakeToken assumes MakeLoginToken has already been called and checks the permissions // specified in the permissionMap. If the logged in user has all those permissions // a JWT will be returned with assertions confirming all those permissions. @@ -40,10 +42,91 @@ type TokenGenerator interface { GetUser() names.UserTag } +// WebsocketConnection represents the websocket connection interface used by the proxy. +type WebsocketConnection interface { + ReadJSON(v interface{}) error + WriteJSON(v interface{}) error + Close() error +} + +// JIMM represents the JIMM interface used by the proxy. +type JIMM interface { + GetOpenFGAUserAndAuthorise(ctx context.Context, email string) (*openfga.User, error) + OAuthAuthenticationService() jimm.OAuthAuthenticator + GetCredentialStore() credentials.CredentialStore +} + +// ProxyHelpers contains all the necessary helpers for proxying a Juju client +// connection to a model. +type ProxyHelpers struct { + ConnClient WebsocketConnection + TokenGen TokenGenerator + ConnectController func(context.Context) (WebsocketConnection, string, error) + AuditLog func(*dbmodel.AuditLogEntry) + JIMM JIMM +} + +// ProxySockets will proxy requests from a client connection through to a controller +// tokenGen is used to authenticate the user and generate JWT token. +// connectController provides the function to return a connection to the desired controller endpoint. +func ProxySockets(ctx context.Context, helpers ProxyHelpers) error { + const op = errors.Op("rpc.ProxySockets") + if helpers.ConnectController == nil { + zapctx.Error(ctx, "Missing controller connect function") + return errors.E(op, "Missing controller connect function") + } + if helpers.AuditLog == nil { + zapctx.Error(ctx, "Missing audit log function") + return errors.E(op, "Missing audit log function") + } + errChan := make(chan error, 2) + msgInFlight := inflightMsgs{messages: make(map[uint64]*message)} + client := writeLockConn{conn: helpers.ConnClient} + // Note that the clProxy start method will create the connection to the desired controller only + // after the first message has been received so that any errors can be properly sent back to the client. + clProxy := clientProxy{ + modelProxy: modelProxy{ + src: &client, + msgs: &msgInFlight, + tokenGen: helpers.TokenGen, + auditLog: helpers.AuditLog, + conversationId: utils.NewConversationID(), + jimm: helpers.JIMM, + }, + errChan: errChan, + createControllerConn: helpers.ConnectController, + } + clProxy.wg.Add(1) + go func() { + defer clProxy.wg.Done() + errChan <- clProxy.start(ctx) + }() + var err error + select { + // No cleanup is needed on error, when the client closes the connection + // all go routines will proceed to error and exit. + case err = <-errChan: + zapctx.Debug(ctx, "Proxy error", zap.Error(err)) + case <-ctx.Done(): + err = errors.E(op, "Context cancelled") + zapctx.Debug(ctx, "Context cancelled") + helpers.ConnClient.Close() + clProxy.mu.Lock() + clProxy.closed = true + // TODO(Kian): Test removing close on dst below. The client connection should do it. + if clProxy.dst != nil { + clProxy.dst.conn.Close() + } + clProxy.mu.Unlock() + } + clProxy.wg.Wait() + return err +} + // writeLockConn provides a websocket connection that is safe for concurrent writes. type writeLockConn struct { mu sync.Mutex - conn *websocket.Conn + conn WebsocketConnection } // readJson allows for non-concurrent reads on the websocket. @@ -58,10 +141,18 @@ func (c *writeLockConn) writeJson(v interface{}) error { return c.conn.WriteJSON(v) } -func (c *writeLockConn) sendMessage(responseData json.RawMessage, request *message) { +func (c *writeLockConn) sendMessage(responseObject any, request *message) { msg := new(message) msg.RequestID = request.RequestID - msg.Response = responseData + msg.Response = request.Response + if responseObject != nil { + responseData, err := json.Marshal(responseObject) + if err != nil { + errorMsg := createErrResponse(err, request) + c.writeJson(errorMsg) + } + msg.Response = responseData + } c.writeJson(msg) } @@ -73,7 +164,10 @@ type inflightMsgs struct { func (msgs *inflightMsgs) addMessage(msg *message) { msgs.mu.Lock() defer msgs.mu.Unlock() + // Putting the login request on ID 0 to persist it. + // Note (alesstimec) It's a bit confusing that we automagically add "login" message + // as the first message. We should revisit this. if msg.Type == "Admin" && msg.Request == "Login" { msgs.messages[0] = msg } else { @@ -103,8 +197,11 @@ type modelProxy struct { msgs *inflightMsgs auditLog func(*dbmodel.AuditLogEntry) tokenGen TokenGenerator + jimm JIMM modelName string conversationId string + + deviceOAuthResponse *oauth2.DeviceAuthResponse } func (p *modelProxy) sendError(socket *writeLockConn, req *message, err error) { @@ -169,7 +266,7 @@ type clientProxy struct { modelProxy wg sync.WaitGroup errChan chan error - createControllerConn func(context.Context) (*websocket.Conn, string, error) + createControllerConn func(context.Context) (WebsocketConnection, string, error) // mu synchronises changes to closed and modelproxy.dst, dst is is only created // at some unspecified point in the future after a client request. mu sync.Mutex @@ -179,7 +276,6 @@ type clientProxy struct { // start begins the client->controller proxier. func (p *clientProxy) start(ctx context.Context) error { const op = errors.Op("rpc.clientProxy.start") - const initialLogin = true defer func() { if p.dst != nil { p.dst.conn.Close() @@ -202,23 +298,18 @@ func (p *clientProxy) start(ctx context.Context) error { p.auditLogMessage(msg, false) // All requests should be proxied as transparently as possible through to the controller // except for auth related requests like Login because JIMM is auth gateway. - if msg.Type == "Admin" && msg.Request == "Login" { - zapctx.Debug(ctx, "Login request found, adding JWT") - if err := addJWT(ctx, initialLogin, msg, nil, p.tokenGen); err != nil { - zapctx.Error(ctx, "Failed to add JWT", zap.Error(err)) - var aerr *auth.AuthenticationError - if stderrors.As(err, &aerr) { - res, err := json.Marshal(aerr.LoginResult) - if err != nil { - p.sendError(p.src, msg, err) - return err - } - p.src.sendMessage(res, msg) - continue - } + if msg.Type == "Admin" { + zapctx.Debug(ctx, "Found an Admin facade call") + toClient, toController, err := p.handleAdminFacade(ctx, msg) + if err != nil { p.sendError(p.src, msg, err) continue } + if toClient != nil { + p.src.sendMessage(nil, toClient) + } else if toController != nil { + msg = toController + } } if msg.RequestID == 0 { zapctx.Error(ctx, "Invalid request ID 0") @@ -381,7 +472,6 @@ func checkPermissionsRequired(ctx context.Context, msg *message) (map[string]any func (p *controllerProxy) redoLogin(ctx context.Context, permissions map[string]any) error { const op = errors.Op("rpc.redoLogin") - const initialLogin = false var loginMsg *message if msg, ok := p.msgs.messages[0]; ok { loginMsg = msg @@ -389,7 +479,7 @@ func (p *controllerProxy) redoLogin(ctx context.Context, permissions map[string] if loginMsg == nil { return errors.E(op, errors.CodeUnauthorized, "Haven't received login yet") } - err := addJWT(ctx, initialLogin, loginMsg, permissions, p.tokenGen) + err := addJWT(ctx, loginMsg, permissions, p.tokenGen) if err != nil { return err } @@ -401,8 +491,7 @@ func (p *controllerProxy) redoLogin(ctx context.Context, permissions map[string] } // addJWT adds a JWT token to the the provided message. -// If initialLogin is set the user will be authenticated. -func addJWT(ctx context.Context, initialLogin bool, msg *message, permissions map[string]interface{}, tokenGen TokenGenerator) error { +func addJWT(ctx context.Context, msg *message, permissions map[string]interface{}, tokenGen TokenGenerator) error { const op = errors.Op("rpc.addJWT") // First we unmarshal the existing LoginRequest. if msg == nil { @@ -412,21 +501,13 @@ func addJWT(ctx context.Context, initialLogin bool, msg *message, permissions ma if err := json.Unmarshal(msg.Params, &lr); err != nil { return errors.E(op, err) } - var jwt []byte - var err error - if initialLogin { - jwt, err = tokenGen.MakeLoginToken(ctx, &lr) - if err != nil { - zapctx.Error(ctx, "failed to make token", zap.Error(err)) - return errors.E(op, err) - } - } else { - jwt, err = tokenGen.MakeToken(ctx, permissions) - if err != nil { - zapctx.Error(ctx, "failed to make token", zap.Error(err)) - return errors.E(op, err) - } + + jwt, err := tokenGen.MakeToken(ctx, permissions) + if err != nil { + zapctx.Error(ctx, "failed to make token", zap.Error(err)) + return errors.E(op, err) } + jwtString := base64.StdEncoding.EncodeToString(jwt) // Add the JWT as base64 encoded string. lr.Token = jwtString @@ -448,71 +529,6 @@ func createErrResponse(err error, req *message) *message { return errMsg } -// ProxyHelpers contains all the necessary helpers for proxying a Juju client -// connection to a model. -type ProxyHelpers struct { - ConnClient *websocket.Conn - TokenGen TokenGenerator - ConnectController func(context.Context) (*websocket.Conn, string, error) - AuditLog func(*dbmodel.AuditLogEntry) -} - -// ProxySockets will proxy requests from a client connection through to a controller -// tokenGen is used to authenticate the user and generate JWT token. -// connectController provides the function to return a connection to the desired controller endpoint. -func ProxySockets(ctx context.Context, helpers ProxyHelpers) error { - const op = errors.Op("rpc.ProxySockets") - if helpers.ConnectController == nil { - zapctx.Error(ctx, "Missing controller connect function") - return errors.E(op, "Missing controller connect function") - } - if helpers.AuditLog == nil { - zapctx.Error(ctx, "Missing audit log function") - return errors.E(op, "Missing audit log function") - } - errChan := make(chan error, 2) - msgInFlight := inflightMsgs{messages: make(map[uint64]*message)} - client := writeLockConn{conn: helpers.ConnClient} - // Note that the clProxy start method will create the connection to the desired controller only - // after the first message has been received so that any errors can be properly sent back to the client. - clProxy := clientProxy{ - modelProxy: modelProxy{ - src: &client, - msgs: &msgInFlight, - tokenGen: helpers.TokenGen, - auditLog: helpers.AuditLog, - conversationId: utils.NewConversationID(), - }, - errChan: errChan, - createControllerConn: helpers.ConnectController, - } - clProxy.wg.Add(1) - go func() { - defer clProxy.wg.Done() - errChan <- clProxy.start(ctx) - }() - var err error - select { - // No cleanup is needed on error, when the client closes the connection - // all go routines will proceed to error and exit. - case err = <-errChan: - zapctx.Debug(ctx, "Proxy error", zap.Error(err)) - case <-ctx.Done(): - err = errors.E(op, "Context cancelled") - zapctx.Debug(ctx, "Context cancelled") - helpers.ConnClient.Close() - clProxy.mu.Lock() - clProxy.closed = true - // TODO(Kian): Test removing close on dst below. The client connection should do it. - if clProxy.dst != nil { - clProxy.dst.conn.Close() - } - clProxy.mu.Unlock() - } - clProxy.wg.Wait() - return err -} - func modifyControllerResponse(msg *message) error { var response map[string]interface{} err := json.Unmarshal(msg.Response, &response) @@ -528,3 +544,121 @@ func modifyControllerResponse(msg *message) error { msg.Response = newResp return nil } + +// handleAdminFacade processes the admin facade call and returns: +// a message to be returned to the source +// a message to be sent to the destination +// an error +func (p *clientProxy) handleAdminFacade(ctx context.Context, msg *message) (clientResponse *message, controllerMessage *message, err error) { + errorFnc := func(err error) (*message, *message, error) { + return nil, nil, err + } + switch msg.Request { + case "LoginDevice": + deviceResponse, err := jimm.LoginDevice(ctx, p.jimm.OAuthAuthenticationService()) + if err != nil { + return errorFnc(err) + } + p.deviceOAuthResponse = deviceResponse + + data, err := json.Marshal(apiparams.LoginDeviceResponse{ + VerificationURI: deviceResponse.VerificationURI, + UserCode: deviceResponse.UserCode, + }) + if err != nil { + return errorFnc(err) + } + msg.Response = data + return msg, nil, nil + case "GetDeviceSessionToken": + sessionToken, err := jimm.GetDeviceSessionToken(ctx, p.jimm.OAuthAuthenticationService(), p.jimm.GetCredentialStore(), p.deviceOAuthResponse) + if err != nil { + return errorFnc(err) + } + data, err := json.Marshal(apiparams.GetDeviceSessionTokenResponse{ + SessionToken: sessionToken, + }) + if err != nil { + return errorFnc(err) + } + msg.Response = data + return msg, nil, nil + case "LoginWithSessionToken": + var request apiparams.LoginWithSessionTokenRequest + err := json.Unmarshal(msg.Params, &request) + if err != nil { + return errorFnc(err) + } + + // Verify the session token + // TODO(CSS-7081): Ensure for tests that the secret key can be configured. + // Or configure cmd tests to use the configured secret. + token, err := p.jimm.OAuthAuthenticationService().VerifySessionToken(request.SessionToken, "test-secret") + if err != nil { + return errorFnc(err) + } + email := token.Subject() + + user, err := p.jimm.GetOpenFGAUserAndAuthorise(ctx, email) + if err != nil { + return errorFnc(err) + } + + jwt, err := p.tokenGen.MakeLoginToken(ctx, user) + if err != nil { + return errorFnc(err) + } + data, err := json.Marshal(params.LoginRequest{ + AuthTag: names.NewUserTag(email).String(), + Token: base64.StdEncoding.EncodeToString(jwt), + }) + if err != nil { + return errorFnc(err) + } + m := *msg + m.Type = "Admin" + m.Request = "Login" + m.Version = 3 + m.Params = data + return nil, &m, nil + case "LoginWithClientCredentials": + var request apiparams.LoginWithClientCredentialsRequest + err := json.Unmarshal(msg.Params, &request) + if err != nil { + return errorFnc(err) + } + err = p.jimm.OAuthAuthenticationService().VerifyClientCredentials(ctx, request.ClientID, request.ClientSecret) + if err != nil { + return errorFnc(err) + } + + user, err := p.jimm.GetOpenFGAUserAndAuthorise(ctx, request.ClientID) + if err != nil { + return errorFnc(err) + } + + jwt, err := p.tokenGen.MakeLoginToken(ctx, user) + if err != nil { + return errorFnc(err) + } + data, err := json.Marshal(params.LoginRequest{ + AuthTag: names.NewUserTag(request.ClientID).String(), + Token: base64.StdEncoding.EncodeToString(jwt), + }) + if err != nil { + return errorFnc(err) + } + m := *msg + m.Type = "Admin" + m.Request = "Login" + m.Version = 3 + m.Params = data + return nil, &m, nil + case "LoginWithCookie": + return errorFnc(errors.E(errors.CodeNotImplemented)) + case "Login": + return errorFnc(errors.E("JIMM does not support login from old clients", errors.CodeNotSupported)) + default: + return nil, nil, nil + } +} diff --git a/internal/rpc/proxy_test.go b/internal/rpc/proxy_test.go new file mode 100644 index 000000000..8b26cc8e8 --- /dev/null +++ b/internal/rpc/proxy_test.go @@ -0,0 +1,428 @@ +// Copyright 2024 Canonical Ltd. + +package rpc_test + +import ( + "context" + "encoding/json" + "sync" + "testing" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + qt "github.com/frankban/quicktest" + "github.com/juju/juju/rpc/params" + "github.com/juju/names/v5" + "github.com/lestrrat-go/jwx/v2/jwt" + "golang.org/x/oauth2" + + apiparams "github.com/canonical/jimm/api/params" + "github.com/canonical/jimm/internal/dbmodel" + "github.com/canonical/jimm/internal/errors" + "github.com/canonical/jimm/internal/jimm" + "github.com/canonical/jimm/internal/jimm/credentials" + "github.com/canonical/jimm/internal/jimmtest" + "github.com/canonical/jimm/internal/openfga" + "github.com/canonical/jimm/internal/rpc" +) + +type message struct { + RequestID uint64 `json:"request-id,omitempty"` + Type string `json:"type,omitempty"` + Version int `json:"version,omitempty"` + ID string `json:"id,omitempty"` + Request string `json:"request,omitempty"` + Params json.RawMessage `json:"params,omitempty"` + Error string `json:"error,omitempty"` + ErrorCode string `json:"error-code,omitempty"` + ErrorInfo map[string]interface{} `json:"error-info,omitempty"` + Response json.RawMessage `json:"response,omitempty"` +} + +func TestProxySocketsAdminFacade(t *testing.T) { + c := qt.New(t) + + const ( + clientID = "test-client-id" + clientSecret = "test-client-secret" + ) + + loginData, err := json.Marshal(params.LoginRequest{ + AuthTag: names.NewUserTag("alice@wonderland.io").String(), + Token: "dGVzdCB0b2tlbg==", + }) + c.Assert(err, qt.IsNil) + + serviceAccountLoginData, err := json.Marshal(params.LoginRequest{ + AuthTag: names.NewUserTag("test-client-id").String(), + Token: "dGVzdCB0b2tlbg==", + }) + c.Assert(err, qt.IsNil) + + ccData, err := json.Marshal(apiparams.LoginWithClientCredentialsRequest{ + ClientID: clientID, + ClientSecret: clientSecret, + }) + c.Assert(err, qt.IsNil) + + tests := []struct { + about string + messageToSend message + expectedClientResponse *message + expectedControllerMessage *message + oauthAuthenticatorError error + }{{ + about: "login device call - client gets response with both user code and verification uri", + messageToSend: message{ + RequestID: 1, + Type: "Admin", + Version: 4, + Request: "LoginDevice", + }, + expectedClientResponse: &message{ + RequestID: 1, + Response: []byte(`{"verification-uri":"http://no-such-uri.canonical.com","user-code":"test user code"}`), + }, + }, { + about: "login device call, but the authenticator returns an error", + messageToSend: message{ + RequestID: 1, + Type: "Admin", + Version: 4, + Request: "LoginDevice", + }, + expectedClientResponse: &message{ + RequestID: 1, + Error: "a silly error", + }, + oauthAuthenticatorError: errors.E("a silly error"), + }, { + about: "get device session token call - client gets response with a session token", + messageToSend: message{ + RequestID: 1, + Type: "Admin", + Version: 4, + Request: "GetDeviceSessionToken", + }, + expectedClientResponse: &message{ + RequestID: 1, + Response: []byte(`{"session-token":"test session token"}`), + }, + }, { + about: "get device session token call, but the authenticator returns an error", + messageToSend: message{ + RequestID: 1, + Type: "Admin", + Version: 4, + Request: "GetDeviceSessionToken", + }, + expectedClientResponse: &message{ + RequestID: 1, + Error: "a silly error", + }, + oauthAuthenticatorError: errors.E("a silly error"), + }, { + about: "login with session token - a login message is sent to the controller", + messageToSend: message{ + RequestID: 1, + Type: "Admin", + Version: 4, + Request: "LoginWithSessionToken", + Params: []byte(`{"client-id": "test session token"}`), + }, + expectedControllerMessage: &message{ + RequestID: 1, + Type: "Admin", + Version: 3, + Request: "Login", + Params: loginData, + }, + }, { + about: "login with session token, but authenticator returns an error", + messageToSend: message{ + RequestID: 1, + Type: "Admin", + Version: 4, + Request: "LoginWithSessionToken", + Params: []byte(`{"client-id": "test session token"}`), + }, + expectedClientResponse: &message{ + RequestID: 1, + Error: "unauthorized access", + ErrorCode: "unauthorized access", + }, + oauthAuthenticatorError: errors.E(errors.CodeUnauthorized), + }, { + about: "login with client credentials - a login message is sent to the controller", + messageToSend: message{ + RequestID: 1, + Type: "Admin", + Version: 4, + Request: "LoginWithClientCredentials", + Params: ccData, + }, + expectedControllerMessage: &message{ + RequestID: 1, + Type: "Admin", + Version: 3, + Request: "Login", + Params: serviceAccountLoginData, + }, + }, { + about: "login with client credentials, but authenticator returns an error", + messageToSend: message{ + RequestID: 1, + Type: "Admin", + Version: 4, + Request: "LoginWithClientCredentials", + Params: ccData, + }, + expectedClientResponse: &message{ + RequestID: 1, + Error: "unauthorized access", + ErrorCode: "unauthorized access", + }, + oauthAuthenticatorError: errors.E(errors.CodeUnauthorized), + }, { + about: "any other message - gets forwarded directly to the controller", + messageToSend: message{ + RequestID: 1, + Type: "Client", + Version: 7, + Request: "AnyMethod", + Params: []byte(`{"key":"value"}`), + }, + expectedControllerMessage: &message{ + RequestID: 1, + Type: "Client", + Version: 7, + Request: "AnyMethod", + Params: []byte(`{"key":"value"}`), + }, + }} + + for _, test := range tests { + t.Run(test.about, func(t *testing.T) { + ctx := context.Background() + clientWebsocket := newMockWebsocketConnection(10) + controllerWebsocket := newMockWebsocketConnection(10) + authenticator := &mockOAuthAuthenticator{ + email: "alice@wonderland.io", + clientID: clientID, + clientSecret: clientSecret, + err: test.oauthAuthenticatorError, + } + + helpers := rpc.ProxyHelpers{ + ConnClient: clientWebsocket, + TokenGen: &mockTokenGenerator{}, + ConnectController: func(ctx context.Context) (rpc.WebsocketConnection, string, error) { + return controllerWebsocket, "test model", nil + }, + AuditLog: func(*dbmodel.AuditLogEntry) {}, + JIMM: &mockJIMM{ + authenticator: authenticator, + }, + } + go rpc.ProxySockets(ctx, helpers) + + data, err := json.Marshal(test.messageToSend) + c.Assert(err, qt.IsNil) + select { + case clientWebsocket.read <- data: + default: + c.Fatal("failed to send message") + } + if test.expectedClientResponse != nil { + select { + case data := <-clientWebsocket.write: + c.Assert(string(data), qt.JSONEquals, test.expectedClientResponse) + case <-time.Tick(10 * time.Minute): + c.Fatal("time out waiting for response") + } + } + if test.expectedControllerMessage != nil { + select { + case data := <-controllerWebsocket.write: + c.Assert(string(data), qt.JSONEquals, test.expectedControllerMessage) + case <-time.Tick(10 * time.Minute): + c.Fatal("time out waiting for response") + } + } + }) + + } +} + +type mockOAuthAuthenticator struct { + jimm.OAuthAuthenticator + + err error + + email string + clientID string + clientSecret string + + updatedEmail string +} + +func (m *mockOAuthAuthenticator) Device(ctx context.Context) (*oauth2.DeviceAuthResponse, error) { + if m.err != nil { + return nil, m.err + } + return &oauth2.DeviceAuthResponse{ + DeviceCode: "test device code", + UserCode: "test user code", + VerificationURI: "http://no-such-uri.canonical.com", + VerificationURIComplete: "http://no-such-uri.canonical.com", + Expiry: time.Now().Add(time.Minute), + Interval: int64(time.Minute.Seconds()), + }, nil +} + +func (m *mockOAuthAuthenticator) DeviceAccessToken(ctx context.Context, res *oauth2.DeviceAuthResponse) (*oauth2.Token, error) { + if m.err != nil { + return nil, m.err + } + return &oauth2.Token{}, nil +} + +func (m *mockOAuthAuthenticator) ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) { + if m.err != nil { + return nil, m.err + } + return &oidc.IDToken{}, nil +} + +func (m *mockOAuthAuthenticator) Email(idToken *oidc.IDToken) (string, error) { + if m.err != nil { + return "", m.err + } + if m.email != "" { + return m.email, nil + } + return "", errors.E(errors.CodeNotFound) +} + +func (m *mockOAuthAuthenticator) UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error { + if m.err != nil { + return m.err + } + m.updatedEmail = email + return nil +} + +func (m *mockOAuthAuthenticator) VerifyClientCredentials(ctx context.Context, clientID string, clientSecret string) error { + if m.err != nil { + return m.err + } + if clientID == m.clientID && clientSecret == m.clientSecret { + return nil + } + return errors.E(errors.CodeUnauthorized) +} + +func (m *mockOAuthAuthenticator) MintSessionToken(email string, secretKey string) (string, error) { + if m.err != nil { + return "", m.err + } + return "test session token", nil +} + +func (m *mockOAuthAuthenticator) VerifySessionToken(token string, secretKey string) (jwt.Token, error) { + if m.err != nil { + return nil, m.err + } + t := jwt.New() + t.Set(jwt.SubjectKey, m.email) + return t, nil +} + +type mockJIMM struct { + authenticator *mockOAuthAuthenticator +} + +func (j *mockJIMM) OAuthAuthenticationService() jimm.OAuthAuthenticator { + return j.authenticator +} + +func (j *mockJIMM) GetOpenFGAUserAndAuthorise(ctx context.Context, email string) (*openfga.User, error) { + return openfga.NewUser( + &dbmodel.Identity{ + Name: email, + }, + nil, + ), nil +} + +func (j *mockJIMM) GetCredentialStore() credentials.CredentialStore { + return jimmtest.NewInMemoryCredentialStore() +} + +func newMockWebsocketConnection(capacity int) *mockWebsocketConnection { + return &mockWebsocketConnection{ + read: make(chan []byte, capacity), + write: make(chan []byte, capacity), + } +} + +type mockWebsocketConnection struct { + read chan []byte + write chan []byte +} + +func (w *mockWebsocketConnection) ReadJSON(v interface{}) error { + data := <-w.read + + return json.Unmarshal(data, v) +} + +func (w *mockWebsocketConnection) WriteJSON(v interface{}) error { + data, err := json.Marshal(v) + if err != nil { + return err + } + w.write <- data + + return nil +} + +func (w *mockWebsocketConnection) Close() error { + close(w.read) + return nil +} + +type mockTokenGenerator struct { + mu sync.RWMutex + + mt names.ModelTag + ct names.ControllerTag + ut names.UserTag +} + +func (m *mockTokenGenerator) MakeLoginToken(ctx context.Context, user *openfga.User) ([]byte, error) { + m.mu.Lock() + defer m.mu.Unlock() + + m.ut = user.ResourceTag() + return []byte("test token"), nil +} + +func (m *mockTokenGenerator) MakeToken(ctx context.Context, permissionMap map[string]interface{}) ([]byte, error) { + return []byte("test token"), nil +} + +func (m *mockTokenGenerator) SetTags(mt names.ModelTag, ct names.ControllerTag) { + m.mu.Lock() + defer m.mu.Unlock() + + m.mt = mt + m.ct = ct +} + +func (m *mockTokenGenerator) GetUser() names.UserTag { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.ut +} From dd76d9e04890d3f85b7b34ac94a3c46c9ccb1c9d Mon Sep 17 00:00:00 2001 From: ale8k Date: Wed, 20 Mar 2024 11:23:18 +0000 Subject: [PATCH 086/126] wshandler tests --- internal/jimmhttp/websocket.go | 50 +++++++++++++++++++---------- internal/jimmhttp/websocket_test.go | 32 ++++++++++++++++++ internal/jimmtest/auth.go | 4 +++ 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/internal/jimmhttp/websocket.go b/internal/jimmhttp/websocket.go index a5c7c166e..0a777e218 100644 --- a/internal/jimmhttp/websocket.go +++ b/internal/jimmhttp/websocket.go @@ -35,15 +35,15 @@ type WSHandler struct { // been started. func (h *WSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctx := req.Context() + var authErr error - ctx, err := handleBrowserAuthentication( - ctx, - h.Server.GetAuthenticationService(), - w, - req, - ) - if err != nil { - return + if h.Server != nil { + ctx, authErr = handleBrowserAuthentication( + ctx, + h.Server.GetAuthenticationService(), + w, + req, + ) } ctx = context.WithValue(ctx, contextPathKey("path"), req.URL.EscapedPath()) @@ -54,28 +54,45 @@ func (h *WSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { zapctx.Error(ctx, "cannot upgrade websocket", zap.Error(err)) return } + servermon.ConcurrentWebsocketConnections.Inc() defer conn.Close() defer servermon.ConcurrentWebsocketConnections.Dec() defer func() { if err := recover(); err != nil { zapctx.Error(ctx, "websocket panic", zap.Any("err", err), zap.Stack("stack")) - data := websocket.FormatCloseMessage(websocket.CloseInternalServerErr, fmt.Sprintf("%v", err)) - if err := conn.WriteControl(websocket.CloseMessage, data, time.Time{}); err != nil { - zapctx.Error(ctx, "cannot write close message", zap.Error(err)) - } + writeInternalServerErrorClosure(ctx, conn, err) } }() + + if authErr != nil { + zapctx.Error(ctx, "browser authentication error", zap.Any("err", authErr), zap.Stack("stack")) + writeInternalServerErrorClosure(ctx, conn, authErr) + return + } + if h.Server == nil { - data := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") - if err := conn.WriteControl(websocket.CloseMessage, data, time.Time{}); err != nil { - zapctx.Error(ctx, "cannot write close message", zap.Error(err)) - } + writeNormalClosure(ctx, conn) return } + h.Server.ServeWS(ctx, conn) } +func writeNormalClosure(ctx context.Context, conn *websocket.Conn) { + data := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") + if err := conn.WriteControl(websocket.CloseMessage, data, time.Time{}); err != nil { + zapctx.Error(ctx, "cannot write close message", zap.Error(err)) + } +} + +func writeInternalServerErrorClosure(ctx context.Context, conn *websocket.Conn, err any) { + data := websocket.FormatCloseMessage(websocket.CloseInternalServerErr, fmt.Sprintf("%v", err)) + if err := conn.WriteControl(websocket.CloseMessage, data, time.Time{}); err != nil { + zapctx.Error(ctx, "cannot write close message", zap.Error(err)) + } +} + // handleBrowserAuthentication handles browser authentication when a session cookie // is present, ultimately placing the identity resolved from the cookie within the // passed context. @@ -106,7 +123,6 @@ func handleBrowserAuthentication(ctx context.Context, authSvc jimm.OAuthAuthenti if err != nil { // Something went wrong when trying to perform the authentication // of the cookie. - w.WriteHeader(http.StatusInternalServerError) return ctx, err } } diff --git a/internal/jimmhttp/websocket_test.go b/internal/jimmhttp/websocket_test.go index d509088c2..8079a0b5b 100644 --- a/internal/jimmhttp/websocket_test.go +++ b/internal/jimmhttp/websocket_test.go @@ -4,6 +4,7 @@ package jimmhttp_test import ( "context" + "net/http" "net/http/httptest" "strings" "testing" @@ -12,8 +13,10 @@ import ( qt "github.com/frankban/quicktest" "github.com/gorilla/websocket" + "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/jimmhttp" + "github.com/canonical/jimm/internal/jimmtest" ) func TestWSHandler(t *testing.T) { @@ -107,3 +110,32 @@ func TestWSHandlerNilServer(t *testing.T) { _, _, err = conn.ReadMessage() c.Assert(err, qt.ErrorMatches, `websocket: close 1000 \(normal\)`) } + +type authFailServer struct{} + +// GetAuthenticationService returns JIMM's oauth authentication service. +func (s authFailServer) GetAuthenticationService() jimm.OAuthAuthenticator { + return jimmtest.NewMockOAuthAuthenticator("") +} + +func (s authFailServer) ServeWS(ctx context.Context, conn *websocket.Conn) {} + +func TestWSHandlerAuthFailsServer(t *testing.T) { + c := qt.New(t) + + hnd := &jimmhttp.WSHandler{ + Server: authFailServer{}, + } + + srv := httptest.NewServer(hnd) + c.Cleanup(srv.Close) + + var d websocket.Dialer + conn, _, err := d.Dial("ws"+strings.TrimPrefix(srv.URL, "http"), http.Header{ + "Cookie": []string{auth.SessionName + "=naughty_cookie"}, + }) + c.Assert(err, qt.IsNil) + + _, _, err = conn.ReadMessage() + c.Assert(err, qt.ErrorMatches, `websocket: close 1011 \(internal server error\): authentication failed`) +} diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index 357442bc6..cd311a58c 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -70,6 +70,10 @@ func (m MockOAuthAuthenticator) VerifySessionToken(token string, secretKey strin return auth.VerifySessionToken(token, m.secretKey) } +func (m MockOAuthAuthenticator) AuthenticateBrowserSession(ctx context.Context, w http.ResponseWriter, req *http.Request) (context.Context, error) { + return ctx, errors.New("authentication failed") +} + // NewUserSessionLogin returns a login provider than be used with Juju Dial Opts // to define how login will take place. In this case we login using a session token // that the JIMM server should verify with the same test secret. From 0d5f3212f64b6edcfda102673e2515e4d3560b1e Mon Sep 17 00:00:00 2001 From: ale8k Date: Wed, 20 Mar 2024 13:02:08 +0000 Subject: [PATCH 087/126] fix nil error passed errors.e --- internal/auth/oauth2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index e0465b124..3fbf01952 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -120,7 +120,7 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP } if params.SessionTokenExpiry == 0 { - return nil, errors.E(op, errors.CodeServerConfiguration, err, "session token expiry not set") + return nil, errors.E(op, errors.CodeServerConfiguration, "session token expiry not set") } return &AuthenticationService{ From 2b176548d30674f375f0db5ce51694a3e954124f Mon Sep 17 00:00:00 2001 From: ale8k Date: Wed, 20 Mar 2024 13:46:50 +0000 Subject: [PATCH 088/126] Remove token expiry check as testing is hard --- internal/auth/oauth2.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 3fbf01952..1abe967ef 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -119,10 +119,6 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP return nil, errors.E(op, errors.CodeServerConfiguration, err, "failed to create oidc provider") } - if params.SessionTokenExpiry == 0 { - return nil, errors.E(op, errors.CodeServerConfiguration, "session token expiry not set") - } - return &AuthenticationService{ provider: provider, oauthConfig: oauth2.Config{ From f7302686582a8cc6f54846a9b0376117d4ca731b Mon Sep 17 00:00:00 2001 From: ale8k Date: Thu, 21 Mar 2024 12:21:55 +0000 Subject: [PATCH 089/126] pr comments --- cmd/jimmsrv/main.go | 12 ++++++------ docker-compose.yaml | 2 +- internal/auth/oauth2.go | 11 ++++++++--- internal/auth/oauth2_test.go | 1 - internal/jimmhttp/websocket.go | 3 ++- internal/jimmtest/auth.go | 3 ++- internal/jujuapi/admin.go | 7 ++++++- 7 files changed, 25 insertions(+), 14 deletions(-) diff --git a/cmd/jimmsrv/main.go b/cmd/jimmsrv/main.go index 20e11aca0..33d2acd3f 100644 --- a/cmd/jimmsrv/main.go +++ b/cmd/jimmsrv/main.go @@ -125,13 +125,13 @@ func start(ctx context.Context, s *service.Service) error { secureSessionCookies = true } - sessionCookieExpiry := os.Getenv("JIMM_SESSION_COOKIE_EXPIRY") - sessionCookieExpiryInt, err := strconv.Atoi(sessionCookieExpiry) + sessionCookieMaxAge := os.Getenv("JIMM_SESSION_COOKIE_MAX_AGE") + sessionCookieMaxAgeInt, err := strconv.Atoi(sessionCookieMaxAge) if err != nil { - return errors.E("unable to parse jimm session cookie expiry") + return errors.E("unable to parse jimm session cookie max age") } - if sessionCookieExpiryInt < 0 { - return errors.E("jimm session cookie expiry cannot be less than 0") + if sessionCookieMaxAgeInt < 0 { + return errors.E("jimm session cookie max age cannot be less than 0") } jimmsvc, err := jimm.NewService(ctx, jimm.Params{ @@ -164,7 +164,7 @@ func start(ctx context.Context, s *service.Service) error { ClientSecret: clientSecret, Scopes: scopesParsed, SessionTokenExpiry: sessionTokenExpiryDuration, - SessionCookieMaxAge: 60, + SessionCookieMaxAge: sessionCookieMaxAgeInt, }, DashboardFinalRedirectURL: os.Getenv("JIMM_DASHBOARD_FINAL_REDIRECT_URL"), SecureSessionCookies: secureSessionCookies, diff --git a/docker-compose.yaml b/docker-compose.yaml index 9a52e2648..c7cf27977 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -78,7 +78,7 @@ services: JIMM_DASHBOARD_FINAL_REDIRECT_URL: "https://my-dashboard.com/final-callback" # Example URL JIMM_ACCESS_TOKEN_EXPIRY_DURATION: 1h JIMM_SECURE_SESSION_COOKIES: false - JIMM_SESSION_COOKIE_EXPIRY: 86400 + JIMM_SESSION_COOKIE_MAX_AGE: 86400 volumes: - ./:/jimm/ - ./local/vault/approle.json:/vault/approle.json:rw diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 1abe967ef..279ed832f 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -12,6 +12,7 @@ import ( "context" "encoding/base64" stderrors "errors" + "fmt" "net/http" "net/mail" "strings" @@ -42,9 +43,11 @@ const ( type sessionIdentityContextKey struct{} -func ContextWithSessionIdentity(ctx context.Context, sessionIdentityId any) context.Context { +func contextWithSessionIdentity(ctx context.Context, sessionIdentityId any) context.Context { return context.WithValue(ctx, sessionIdentityContextKey{}, sessionIdentityId) } + +// SessionIdentityFromContext returns the session identity key from the context. func SessionIdentityFromContext(ctx context.Context) string { v := ctx.Value(sessionIdentityContextKey{}) if v == nil { @@ -428,7 +431,7 @@ func (as *AuthenticationService) AuthenticateBrowserSession(ctx context.Context, return ctx, errors.E(op, err) } - ctx = ContextWithSessionIdentity(ctx, identityId) + ctx = contextWithSessionIdentity(ctx, identityId) if err := as.extendSession(session, w, req); err != nil { return ctx, errors.E(op, err) @@ -444,7 +447,7 @@ func (as *AuthenticationService) validateAndUpdateAccessToken(ctx context.Contex emailStr, ok := email.(string) if !ok { - return errors.E(op, "failed to cast email") + return errors.E(op, fmt.Sprintf("failed to cast email: got %T, expected %T", email, emailStr)) } db := as.db @@ -462,6 +465,8 @@ func (as *AuthenticationService) validateAndUpdateAccessToken(ctx context.Contex TokenType: u.AccessTokenType, } + // Valid simply checks the expiry, if the token isn't valid, + // we attempt to refresh the identities tokens and update them. if t.Valid() { return nil } diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 178a7d8e1..0107dce17 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -364,7 +364,6 @@ func TestAuthenticateBrowserSessionRejectsNoneDecryptableOrDecodableCookies(t *t rec = httptest.NewRecorder() - // The underlying error is a a value not valid err _, err = authSvc.AuthenticateBrowserSession(ctx, rec, req) c.Assert(err, qt.ErrorMatches, "failed to retrieve session") } diff --git a/internal/jimmhttp/websocket.go b/internal/jimmhttp/websocket.go index 0a777e218..821056130 100644 --- a/internal/jimmhttp/websocket.go +++ b/internal/jimmhttp/websocket.go @@ -37,7 +37,7 @@ func (h *WSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctx := req.Context() var authErr error - if h.Server != nil { + if h.Server != nil && h.Server.GetAuthenticationService() != nil { ctx, authErr = handleBrowserAuthentication( ctx, h.Server.GetAuthenticationService(), @@ -121,6 +121,7 @@ func handleBrowserAuthentication(ctx context.Context, authSvc jimm.OAuthAuthenti ctx, w, req, ) if err != nil { + zapctx.Error(ctx, "authenticate browser session failed", zap.Error(err)) // Something went wrong when trying to perform the authentication // of the cookie. return ctx, err diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index cd311a58c..b46e1ee08 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -14,6 +14,7 @@ import ( "net/http/httptest" "net/url" "regexp" + "strconv" "strings" "time" @@ -110,7 +111,7 @@ func SetupTestDashboardCallbackHandler(browserURL string, db *db.Database, sessi if err != nil { return nil, err } - port := fmt.Sprintf("%d", listener.Addr().(*net.TCPAddr).Port) + port := strconv.Itoa(listener.Addr().(*net.TCPAddr).Port) // Create unstarted server to enable auth service s := httptest.NewUnstartedServer(nil) diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index a49066105..81a143f4e 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -71,7 +71,12 @@ func (r *controllerRoot) GetDeviceSessionToken(ctx context.Context) (params.GetD return response, nil } -// LoginWithSessionCookie +// LoginWithSessionCookie is a facade call which has the cookie intercepted at the http layer, +// in which it is then placed on the controller root under "identityId", this identityId is used +// to perform a user lookup and authorise the login call. +// +// It may be misleading in that it does not interact with cookies at all, but this will only ever +// be successful upon the http layer login being successful. func (r *controllerRoot) LoginWithSessionCookie(ctx context.Context) (jujuparams.LoginResult, error) { const op = errors.Op("jujuapi.LoginWithSessionCookie") From 2efd6c783bf82bee9cf0187496465513336aed9e Mon Sep 17 00:00:00 2001 From: ale8k Date: Thu, 21 Mar 2024 13:41:46 +0000 Subject: [PATCH 090/126] pr changes --- internal/auth/oauth2.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 279ed832f..3711dfa01 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -503,9 +503,7 @@ func (as *AuthenticationService) refreshIdentitiesToken(ctx context.Context, ema func (as *AuthenticationService) deleteSession(session *sessions.Session, w http.ResponseWriter, req *http.Request) error { const op = errors.Op("auth.AuthenticationService.deleteSession") - session.Options.MaxAge = -1 - - if err := session.Save(req, w); err != nil { + if err := as.modifySession(session, w, req, -1); err != nil { return errors.E(op, err) } @@ -515,7 +513,17 @@ func (as *AuthenticationService) deleteSession(session *sessions.Session, w http func (as *AuthenticationService) extendSession(session *sessions.Session, w http.ResponseWriter, req *http.Request) error { const op = errors.Op("auth.AuthenticationService.extendSession") - session.Options.MaxAge = as.sessionCookieMaxAge + if err := as.modifySession(session, w, req, as.sessionCookieMaxAge); err != nil { + return errors.E(op, err) + } + + return nil +} + +func (as *AuthenticationService) modifySession(session *sessions.Session, w http.ResponseWriter, req *http.Request, maxAge int) error { + const op = errors.Op("auth.AuthenticationService.modifySession") + + session.Options.MaxAge = maxAge if err := session.Save(req, w); err != nil { return errors.E(op, err) From 6de58e053ead9bc666cf6c773f6e47d4bc9d070c Mon Sep 17 00:00:00 2001 From: ale8k Date: Thu, 21 Mar 2024 13:43:31 +0000 Subject: [PATCH 091/126] dd --- internal/auth/oauth2.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 3711dfa01..3dc47a86a 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -423,7 +423,6 @@ func (as *AuthenticationService) AuthenticateBrowserSession(ctx context.Context, } err = as.validateAndUpdateAccessToken(ctx, identityId) - if err != nil { if err := as.deleteSession(session, w, req); err != nil { return ctx, errors.E(op, err) From efbf21c1fbe1d149b52510fd51ad847e143c010f Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Fri, 22 Mar 2024 08:41:25 +0000 Subject: [PATCH 092/126] Browser cookie sessions (#1178) Implements browser sessions via cookies and persistent session storage. --- cmd/jimmsrv/main.go | 22 +- docker-compose.yaml | 2 +- go.mod | 3 +- go.sum | 4 + internal/auth/oauth2.go | 199 +++++++++++++++++- internal/auth/oauth2_test.go | 252 +++++++++++++++++++++-- internal/cmdtest/jimmsuite.go | 9 +- internal/dbmodel/identity.go | 7 + internal/dbmodel/sql/postgres/1_6.sql | 2 + internal/jimm/jimm.go | 10 +- internal/jimm/user_test.go | 19 +- internal/jimmhttp/auth_handler.go | 43 ++-- internal/jimmhttp/auth_handler_test.go | 121 +---------- internal/jimmhttp/websocket.go | 89 +++++++- internal/jimmhttp/websocket_test.go | 43 ++++ internal/jimmjwx/utils_test.go | 9 +- internal/jimmtest/auth.go | 154 ++++++++++++++ internal/jimmtest/suite.go | 4 +- internal/jujuapi/admin.go | 39 ++++ internal/jujuapi/admin_test.go | 149 +++++++++++++- internal/jujuapi/controllerroot.go | 7 +- internal/jujuapi/export_test.go | 2 +- internal/jujuapi/pinger_internal_test.go | 2 +- internal/jujuapi/websocket.go | 16 +- internal/jujuapi/websocket_test.go | 32 ++- service.go | 22 +- service_test.go | 72 ++++--- 27 files changed, 1075 insertions(+), 258 deletions(-) diff --git a/cmd/jimmsrv/main.go b/cmd/jimmsrv/main.go index e9918c682..33d2acd3f 100644 --- a/cmd/jimmsrv/main.go +++ b/cmd/jimmsrv/main.go @@ -125,13 +125,13 @@ func start(ctx context.Context, s *service.Service) error { secureSessionCookies = true } - sessionCookieExpiry := os.Getenv("JIMM_SESSION_COOKIE_EXPIRY") - sessionCookieExpiryInt, err := strconv.Atoi(sessionCookieExpiry) + sessionCookieMaxAge := os.Getenv("JIMM_SESSION_COOKIE_MAX_AGE") + sessionCookieMaxAgeInt, err := strconv.Atoi(sessionCookieMaxAge) if err != nil { - return errors.E("unable to parse jimm session cookie expiry") + return errors.E("unable to parse jimm session cookie max age") } - if sessionCookieExpiryInt < 0 { - return errors.E("jimm session cookie expiry cannot be less than 0") + if sessionCookieMaxAgeInt < 0 { + return errors.E("jimm session cookie max age cannot be less than 0") } jimmsvc, err := jimm.NewService(ctx, jimm.Params{ @@ -159,15 +159,15 @@ func start(ctx context.Context, s *service.Service) error { JWTExpiryDuration: jwtExpiryDuration, InsecureSecretStorage: insecureSecretStorage, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: issuerURL, - ClientID: clientID, - ClientSecret: clientSecret, - Scopes: scopesParsed, - SessionTokenExpiry: sessionTokenExpiryDuration, + IssuerURL: issuerURL, + ClientID: clientID, + ClientSecret: clientSecret, + Scopes: scopesParsed, + SessionTokenExpiry: sessionTokenExpiryDuration, + SessionCookieMaxAge: sessionCookieMaxAgeInt, }, DashboardFinalRedirectURL: os.Getenv("JIMM_DASHBOARD_FINAL_REDIRECT_URL"), SecureSessionCookies: secureSessionCookies, - SessionCookieExpiry: sessionCookieExpiryInt, }) if err != nil { return err diff --git a/docker-compose.yaml b/docker-compose.yaml index 9a52e2648..c7cf27977 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -78,7 +78,7 @@ services: JIMM_DASHBOARD_FINAL_REDIRECT_URL: "https://my-dashboard.com/final-callback" # Example URL JIMM_ACCESS_TOKEN_EXPIRY_DURATION: 1h JIMM_SECURE_SESSION_COOKIES: false - JIMM_SESSION_COOKIE_EXPIRY: 86400 + JIMM_SESSION_COOKIE_MAX_AGE: 86400 volumes: - ./:/jimm/ - ./local/vault/approle.json:/vault/approle.json:rw diff --git a/go.mod b/go.mod index a27924975..d61ff3809 100644 --- a/go.mod +++ b/go.mod @@ -132,7 +132,7 @@ require ( github.com/godbus/dbus/v5 v5.0.4 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.0.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect @@ -251,6 +251,7 @@ require ( github.com/muhlemmer/gu v0.3.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/oracle/oci-go-sdk/v65 v65.55.0 // indirect github.com/packethost/packngo v0.28.1 // indirect diff --git a/go.sum b/go.sum index d82c9d791..d00d9fbe1 100644 --- a/go.sum +++ b/go.sum @@ -296,6 +296,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -881,6 +883,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= +github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk= +github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index c98b7e802..3dc47a86a 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -12,11 +12,14 @@ import ( "context" "encoding/base64" stderrors "errors" + "fmt" + "net/http" "net/mail" "strings" "time" "github.com/coreos/go-oidc/v3/oidc" + "github.com/gorilla/sessions" "github.com/juju/zaputil/zapctx" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwt" @@ -28,6 +31,31 @@ import ( "github.com/canonical/jimm/internal/errors" ) +const ( + // SessionName is the name of the gorilla session and is used to retrieve + // the session object from the database. + SessionName = "jimm-browser-session" + + // SessionIdentityKey is the key for the identity value stored within the + // session. + SessionIdentityKey = "identity-id" +) + +type sessionIdentityContextKey struct{} + +func contextWithSessionIdentity(ctx context.Context, sessionIdentityId any) context.Context { + return context.WithValue(ctx, sessionIdentityContextKey{}, sessionIdentityId) +} + +// SessionIdentityFromContext returns the session identity key from the context. +func SessionIdentityFromContext(ctx context.Context) string { + v := ctx.Value(sessionIdentityContextKey{}) + if v == nil { + return "" + } + return v.(string) +} + // AuthenticationService handles authentication within JIMM. type AuthenticationService struct { oauthConfig oauth2.Config @@ -37,7 +65,12 @@ type AuthenticationService struct { // sessionTokenExpiry holds the expiry time for JIMM minted session tokens (JWTs). sessionTokenExpiry time.Duration + // sessionCookieMaxAge holds the max age for session cookies. + sessionCookieMaxAge int + db IdentityStore + + sessionStore sessions.Store } // Identity store holds the necessary methods to get and update an identity @@ -62,6 +95,8 @@ type AuthenticationServiceParams struct { Scopes []string // SessionTokenExpiry holds the expiry time of minted JIMM session tokens (JWTs). SessionTokenExpiry time.Duration + // SessionCookieMaxAge holds the max age for session cookies. + SessionCookieMaxAge int // RedirectURL is the URL for handling the exchange of authorisation // codes into access tokens (and id tokens), for JIMM, this is expected // to be the servers own callback endpoint registered under /auth/callback. @@ -71,6 +106,9 @@ type AuthenticationServiceParams struct { // to fetch and update identities. I.e., their access tokens, refresh tokens, // display name, etc. Store IdentityStore + + // SessionStore holds the store for creating, getting and saving gorrila sessions. + SessionStore sessions.Store } // NewAuthenticationService returns a new authentication service for handling @@ -93,8 +131,10 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP Scopes: params.Scopes, RedirectURL: params.RedirectURL, }, - sessionTokenExpiry: params.SessionTokenExpiry, - db: params.Store, + sessionTokenExpiry: params.SessionTokenExpiry, + db: params.Store, + sessionStore: params.SessionStore, + sessionCookieMaxAge: params.SessionCookieMaxAge, }, nil } @@ -277,6 +317,8 @@ func (as *AuthenticationService) UpdateIdentity(ctx context.Context, email strin u.AccessToken = token.AccessToken u.RefreshToken = token.RefreshToken + u.AccessTokenExpiry = token.Expiry + u.AccessTokenType = token.TokenType if err := db.UpdateIdentity(ctx, u); err != nil { return errors.E(op, err) } @@ -335,3 +377,156 @@ func (as *AuthenticationService) VerifyClientCredentials(ctx context.Context, cl } return nil } + +// CreateBrowserSession creates a session and updates the cookie for a browser +// login callback. +func (as *AuthenticationService) CreateBrowserSession( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + secureCookies bool, + email string, +) error { + const op = errors.Op("auth.AuthenticationService.CreateBrowserSession") + + session, err := as.sessionStore.Get(r, SessionName) + if err != nil { + return errors.E(op, err) + } + + session.IsNew = true // Sets cookie to a fresh new cookie + session.Options.MaxAge = as.sessionCookieMaxAge // Expiry in seconds + session.Options.Secure = secureCookies // Ensures only sent with HTTPS + session.Options.HttpOnly = false // Allow Javascript to read it + + session.Values[SessionIdentityKey] = email + if err = session.Save(r, w); err != nil { + return errors.E(op, err) + } + return nil +} + +// AuthenticateBrowserSession updates the session for a browser, additionally +// retrieving new access tokens upon expiry. If this cannot be done, the cookie +// is deleted and an error is returned. +func (as *AuthenticationService) AuthenticateBrowserSession(ctx context.Context, w http.ResponseWriter, req *http.Request) (context.Context, error) { + const op = errors.Op("auth.AuthenticationService.AuthenticateBrowserSession") + + session, err := as.sessionStore.Get(req, SessionName) + if err != nil { + return ctx, errors.E(op, err, "failed to retrieve session") + } + + identityId, ok := session.Values[SessionIdentityKey] + if !ok { + return ctx, errors.E(op, "session is missing identity key") + } + + err = as.validateAndUpdateAccessToken(ctx, identityId) + if err != nil { + if err := as.deleteSession(session, w, req); err != nil { + return ctx, errors.E(op, err) + } + return ctx, errors.E(op, err) + } + + ctx = contextWithSessionIdentity(ctx, identityId) + + if err := as.extendSession(session, w, req); err != nil { + return ctx, errors.E(op, err) + } + + return ctx, nil +} + +// validateAndUpdateAccessToken validates the access tokens expiry, and if it cannot, then +// it attempts to refresh the access token. +func (as *AuthenticationService) validateAndUpdateAccessToken(ctx context.Context, email any) error { + const op = errors.Op("auth.AuthenticationService.validateAndUpdateAccessToken") + + emailStr, ok := email.(string) + if !ok { + return errors.E(op, fmt.Sprintf("failed to cast email: got %T, expected %T", email, emailStr)) + } + + db := as.db + u := &dbmodel.Identity{ + Name: emailStr, + } + if err := db.GetIdentity(ctx, u); err != nil { + return errors.E(op, err) + } + + t := &oauth2.Token{ + AccessToken: u.AccessToken, + RefreshToken: u.RefreshToken, + Expiry: u.AccessTokenExpiry, + TokenType: u.AccessTokenType, + } + + // Valid simply checks the expiry, if the token isn't valid, + // we attempt to refresh the identities tokens and update them. + if t.Valid() { + return nil + } + + if err := as.refreshIdentitiesToken(ctx, emailStr, t); err != nil { + return errors.E(op, err) + } + + return nil +} + +// refreshIdentitiesToken creates a token source based on the expired token and performs +// a manual token refresh, updating the identity afterwards. +// +// This is to be called only when a token is expired. +func (as *AuthenticationService) refreshIdentitiesToken(ctx context.Context, email string, t *oauth2.Token) error { + const op = errors.Op("auth.AuthenticationService.refreshIdentitiesToken") + + tSrc := as.oauthConfig.TokenSource(ctx, t) + + // Get a new access and refresh token (token source only has Token()) + newToken, err := tSrc.Token() + if err != nil { + return errors.E(op, err, "failed to refresh token") + } + + if err := as.UpdateIdentity(ctx, email, newToken); err != nil { + return errors.E(op, err, "failed to update identity") + } + + return nil +} + +func (as *AuthenticationService) deleteSession(session *sessions.Session, w http.ResponseWriter, req *http.Request) error { + const op = errors.Op("auth.AuthenticationService.deleteSession") + + if err := as.modifySession(session, w, req, -1); err != nil { + return errors.E(op, err) + } + + return nil +} + +func (as *AuthenticationService) extendSession(session *sessions.Session, w http.ResponseWriter, req *http.Request) error { + const op = errors.Op("auth.AuthenticationService.extendSession") + + if err := as.modifySession(session, w, req, as.sessionCookieMaxAge); err != nil { + return errors.E(op, err) + } + + return nil +} + +func (as *AuthenticationService) modifySession(session *sessions.Session, w http.ResponseWriter, req *http.Request, maxAge int) error { + const op = errors.Op("auth.AuthenticationService.modifySession") + + session.Options.MaxAge = maxAge + + if err := session.Save(req, w); err != nil { + return errors.E(op, err) + } + + return nil +} diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index d07e9e6e6..0107dce17 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -4,41 +4,53 @@ package auth_test import ( "context" + "encoding/base64" "fmt" "io" "net/http" "net/http/cookiejar" + "net/http/httptest" "net/url" "regexp" "testing" "time" + "github.com/antonlindstrom/pgstore" "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/jimmtest" "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" + "github.com/gorilla/sessions" ) -func setupTestAuthSvc(ctx context.Context, c *qt.C, expiry time.Duration) (*auth.AuthenticationService, *db.Database) { +func setupTestAuthSvc(ctx context.Context, c *qt.C, expiry time.Duration) (*auth.AuthenticationService, *db.Database, sessions.Store) { db := &db.Database{ DB: jimmtest.PostgresDB(c, func() time.Time { return time.Now() }), } c.Assert(db.Migrate(ctx, false), qt.IsNil) + sqldb, err := db.DB.DB() + c.Assert(err, qt.IsNil) + + sessionStore, err := pgstore.NewPGStoreFromPool(sqldb, []byte("secretsecretdigletts")) + c.Assert(err, qt.IsNil) + authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: expiry, - RedirectURL: "http://localhost:8080/auth/callback", - Store: db, + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: expiry, + RedirectURL: "http://localhost:8080/auth/callback", + Store: db, + SessionStore: sessionStore, + SessionCookieMaxAge: 60, }) c.Assert(err, qt.IsNil) - return authSvc, db + return authSvc, db, sessionStore } // This test requires the local docker compose to be running and keycloak @@ -49,7 +61,7 @@ func TestAuthCodeURL(t *testing.T) { c := qt.New(t) ctx := context.Background() - authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, _, _ := setupTestAuthSvc(ctx, c, time.Hour) url := authSvc.AuthCodeURL() c.Assert( @@ -75,7 +87,7 @@ func TestDevice(t *testing.T) { ctx := context.Background() - authSvc, db := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, db, _ := setupTestAuthSvc(ctx, c, time.Hour) res, err := authSvc.Device(ctx) c.Assert(err, qt.IsNil) @@ -166,7 +178,7 @@ func TestSessionTokens(t *testing.T) { ctx := context.Background() - authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, _, _ := setupTestAuthSvc(ctx, c, time.Hour) secretKey := "secret-key" token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) @@ -183,7 +195,7 @@ func TestSessionTokenRejectsWrongSecretKey(t *testing.T) { ctx := context.Background() - authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, _, _ := setupTestAuthSvc(ctx, c, time.Hour) secretKey := "secret-key" token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) @@ -200,7 +212,7 @@ func TestSessionTokenRejectsExpiredToken(t *testing.T) { ctx := context.Background() noDuration := time.Duration(0) - authSvc, _ := setupTestAuthSvc(ctx, c, noDuration) + authSvc, _, _ := setupTestAuthSvc(ctx, c, noDuration) secretKey := "secret-key" token, err := authSvc.MintSessionToken("jimm-test@canonical.com", secretKey) @@ -216,7 +228,7 @@ func TestSessionTokenValidatesEmail(t *testing.T) { ctx := context.Background() - authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, _, _ := setupTestAuthSvc(ctx, c, time.Hour) secretKey := "secret-key" token, err := authSvc.MintSessionToken("", secretKey) @@ -237,7 +249,7 @@ func TestVerifyClientCredentials(t *testing.T) { validClientSecret = "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf" ) - authSvc, _ := setupTestAuthSvc(ctx, c, time.Hour) + authSvc, _, _ := setupTestAuthSvc(ctx, c, time.Hour) err := authSvc.VerifyClientCredentials(ctx, validClientID, validClientSecret) c.Assert(err, qt.IsNil) @@ -245,3 +257,211 @@ func TestVerifyClientCredentials(t *testing.T) { err = authSvc.VerifyClientCredentials(ctx, "invalid-client-id", validClientSecret) c.Assert(err, qt.ErrorMatches, "invalid client credentials") } + +func assertSetCookiesIsCorrect(c *qt.C, rec *httptest.ResponseRecorder, parsedCookies []*http.Cookie) { + assertHasCookie := func(name string, cookies []*http.Cookie) { + found := false + for _, v := range cookies { + if v.Name == name { + found = true + } + } + c.Assert(found, qt.IsTrue) + } + assertHasCookie(auth.SessionName, parsedCookies) + assertHasCookie("Path", parsedCookies) + assertHasCookie("Expires", parsedCookies) + assertHasCookie("Max-Age", parsedCookies) +} + +func TestCreateBrowserSession(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + + authSvc, _, sessionStore := setupTestAuthSvc(ctx, c, time.Hour) + + rec := httptest.NewRecorder() + req, err := http.NewRequest("GET", "", nil) + c.Assert(err, qt.IsNil) + + err = authSvc.CreateBrowserSession(ctx, rec, req, false, "jimm-test@canonical.com") + c.Assert(err, qt.IsNil) + + cookies := rec.Header().Get("Set-Cookie") + parsedCookies := jimmtest.ParseCookies(cookies) + assertSetCookiesIsCorrect(c, rec, parsedCookies) + + req.AddCookie(&http.Cookie{ + Name: auth.SessionName, + Value: parsedCookies[0].Value, + }) + + session, err := sessionStore.Get(req, auth.SessionName) + c.Assert(err, qt.IsNil) + c.Assert(session.Values[auth.SessionIdentityKey], qt.Equals, "jimm-test@canonical.com") +} + +func TestAuthenticateBrowserSession(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + + authSvc, db, sessionStore := setupTestAuthSvc(ctx, c, time.Hour) + + cookie, err := jimmtest.RunBrowserLogin(db, sessionStore) + c.Assert(err, qt.IsNil) + + rec := httptest.NewRecorder() + req, err := http.NewRequest("GET", "", nil) + c.Assert(err, qt.IsNil) + + cookies := jimmtest.ParseCookies(cookie) + + req.AddCookie(cookies[0]) + + ctx, err = authSvc.AuthenticateBrowserSession(ctx, rec, req) + c.Assert(err, qt.IsNil) + + // Check identity added + identityId := auth.SessionIdentityFromContext(ctx) + c.Assert(identityId, qt.Equals, "jimm-test@canonical.com") + + // Assert Set-Cookie present + setCookieCookies := rec.Header().Get("Set-Cookie") + parsedCookies := jimmtest.ParseCookies(setCookieCookies) + assertSetCookiesIsCorrect(c, rec, parsedCookies) +} + +func TestAuthenticateBrowserSessionRejectsNoneDecryptableOrDecodableCookies(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + + authSvc, db, sessionStore := setupTestAuthSvc(ctx, c, time.Hour) + + _, err := jimmtest.RunBrowserLogin(db, sessionStore) + c.Assert(err, qt.IsNil) + + // Failure case 1: Bad base64 decoding + req, err := http.NewRequest("GET", "", nil) + c.Assert(err, qt.IsNil) + req.AddCookie(&http.Cookie{ + Name: auth.SessionName, + Value: "bad cookie, very naughty, bad bad cookie", + }) + + rec := httptest.NewRecorder() + + // The underlying error is a failed base64 decode + _, err = authSvc.AuthenticateBrowserSession(ctx, rec, req) + c.Assert(err, qt.ErrorMatches, "failed to retrieve session") + + // Failure case 2: Value isn't valid but is base64 decoded + req, err = http.NewRequest("GET", "", nil) + c.Assert(err, qt.IsNil) + req.AddCookie(&http.Cookie{ + Name: auth.SessionName, + Value: base64.StdEncoding.EncodeToString([]byte("bad cookie, very naughty, bad bad cookie")), + }) + + rec = httptest.NewRecorder() + + _, err = authSvc.AuthenticateBrowserSession(ctx, rec, req) + c.Assert(err, qt.ErrorMatches, "failed to retrieve session") +} + +func TestAuthenticateBrowserSessionHandlesExpiredAccessTokens(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + + authSvc, db, sessionStore := setupTestAuthSvc(ctx, c, time.Hour) + + cookie, err := jimmtest.RunBrowserLogin(db, sessionStore) + c.Assert(err, qt.IsNil) + + rec := httptest.NewRecorder() + req, err := http.NewRequest("GET", "", nil) + c.Assert(err, qt.IsNil) + + cookies := jimmtest.ParseCookies(cookie) + + req.AddCookie(cookies[0]) + + // User exists from run browser login, but we're gonna + // artificially expire their access token + u := dbmodel.Identity{ + Name: "jimm-test@canonical.com", + } + err = db.GetIdentity(ctx, &u) + c.Assert(err, qt.IsNil) + + previousToken := u.AccessToken + + u.AccessTokenExpiry = time.Now() + db.UpdateIdentity(ctx, &u) + + ctx, err = authSvc.AuthenticateBrowserSession(ctx, rec, req) + c.Assert(err, qt.IsNil) + + // Check identity added + identityId := auth.SessionIdentityFromContext(ctx) + c.Assert(identityId, qt.Equals, "jimm-test@canonical.com") + + // Get identity again with new access token expiry and access token + err = db.GetIdentity(ctx, &u) + c.Assert(err, qt.IsNil) + + // Assert new access token is valid for at least 4 minutes(our setup is 5 minutes) + c.Assert(u.AccessTokenExpiry.After(time.Now().Add(time.Minute*4)), qt.IsTrue) + // Assert its not the same token as previous token + c.Assert(u.AccessToken, qt.Not(qt.Equals), previousToken) + // Assert Set-Cookie present + setCookieCookies := rec.Header().Get("Set-Cookie") + parsedCookies := jimmtest.ParseCookies(setCookieCookies) + assertSetCookiesIsCorrect(c, rec, parsedCookies) +} + +func TestAuthenticateBrowserSessionHandlesMissingOrExpiredRefreshTokens(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + + authSvc, db, sessionStore := setupTestAuthSvc(ctx, c, time.Hour) + + cookie, err := jimmtest.RunBrowserLogin(db, sessionStore) + c.Assert(err, qt.IsNil) + + rec := httptest.NewRecorder() + req, err := http.NewRequest("GET", "", nil) + c.Assert(err, qt.IsNil) + + cookies := jimmtest.ParseCookies(cookie) + + req.AddCookie(cookies[0]) + + // User exists from run browser login, but we're gonna + // artificially expire their access token + u := dbmodel.Identity{ + Name: "jimm-test@canonical.com", + } + err = db.GetIdentity(ctx, &u) + c.Assert(err, qt.IsNil) + + // As our access token has "expired" + u.AccessTokenExpiry = time.Now() + // And we're missing a refresh token (the same case would apply for an expired refresh token + // or any scenario where the token source cannot refresh the access token) + u.RefreshToken = "" + db.UpdateIdentity(ctx, &u) + + // AuthenticateBrowserSession should fail to refresh the users session and delete + // the current session, giving us the same cookie back with a max-age of -1. + _, err = authSvc.AuthenticateBrowserSession(ctx, rec, req) + c.Assert(err, qt.ErrorMatches, ".*failed to refresh token.*") + + // Assert that the header to delete the session is set correctly based + // on a failed access token refresh due to refresh token issues. + setCookieCookies := rec.Header().Get("Set-Cookie") + c.Assert( + setCookieCookies, + qt.Equals, + "jimm-browser-session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0", + ) +} diff --git a/internal/cmdtest/jimmsuite.go b/internal/cmdtest/jimmsuite.go index 50de3118f..c35b1eb5b 100644 --- a/internal/cmdtest/jimmsuite.go +++ b/internal/cmdtest/jimmsuite.go @@ -83,10 +83,11 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { JWTExpiryDuration: time.Minute, InsecureSecretStorage: true, OAuthAuthenticatorParams: service.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", } diff --git a/internal/dbmodel/identity.go b/internal/dbmodel/identity.go index 75607fc65..1888af930 100644 --- a/internal/dbmodel/identity.go +++ b/internal/dbmodel/identity.go @@ -4,6 +4,7 @@ package dbmodel import ( "database/sql" + "time" jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" @@ -44,6 +45,12 @@ type Identity struct { // from the browser or device flow, and as such is updated on every successful // login. RefreshToken string + + // AccessTokenExpiry is the expiration date for this access token. + AccessTokenExpiry time.Time + + // AccessTokenType is the type for the token, typically bearer. + AccessTokenType string } // Tag returns a names.Tag for the identity. diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql index d5ba10f6d..5f380c483 100644 --- a/internal/dbmodel/sql/postgres/1_6.sql +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -2,6 +2,8 @@ -- and is a migration that renames `user` to `identity`. ALTER TABLE users ADD COLUMN access_token TEXT; ALTER TABLE users ADD COLUMN refresh_token TEXT; +ALTER TABLE users ADD COLUMN access_token_expiry TIMESTAMP; +ALTER TABLE users ADD COLUMN access_token_type TEXT; -- Note that we don't need to rename underlying indexes/constraints. As Postgres -- docs states: diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index 388690fd1..fbf22e097 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -7,10 +7,10 @@ package jimm import ( "context" "database/sql" + "net/http" "strings" "time" - "github.com/antonlindstrom/pgstore" "github.com/coreos/go-oidc/v3/oidc" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/juju/api/base" @@ -85,9 +85,6 @@ type JIMM struct { // OAuthAuthenticator is responsible for handling authentication // via OAuth2.0 AND JWT access tokens to JIMM. OAuthAuthenticator OAuthAuthenticator - - // CookieSessionStore is respnsible for handling cookie based sessions. - CookieSessionStore *pgstore.PGStore } // OAuthAuthenticationService returns the JIMM's authentication service. @@ -164,6 +161,11 @@ type OAuthAuthenticator interface { // VerifyClientCredentials verifies the provided client ID and client secret. VerifyClientCredentials(ctx context.Context, clientID string, clientSecret string) error + + // AuthenticateBrowserSession updates the session for a browser, additionally + // retrieving new access tokens upon expiry. If this cannot be done, the cookie + // is deleted and an error is returned. + AuthenticateBrowserSession(ctx context.Context, w http.ResponseWriter, req *http.Request) (context.Context, error) } // GetCredentialStore returns the credential store used by JIMM. diff --git a/internal/jimm/user_test.go b/internal/jimm/user_test.go index 1e55c516d..bae5f7b52 100644 --- a/internal/jimm/user_test.go +++ b/internal/jimm/user_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/antonlindstrom/pgstore" qt "github.com/frankban/quicktest" "github.com/juju/names/v5" @@ -29,13 +30,19 @@ func TestGetOpenFGAUser(t *testing.T) { db := &db.Database{ DB: jimmtest.PostgresDB(c, func() time.Time { return time.Now() }), } - // TODO(ale8k): Mock this + sqldb, err := db.DB.DB() + c.Assert(err, qt.IsNil) + + sessionStore, err := pgstore.NewPGStoreFromPool(sqldb, []byte("secretsecretdigletts")) + c.Assert(err, qt.IsNil) authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{"openid", "profile", "email"}, - SessionTokenExpiry: time.Hour, - Store: db, + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{"openid", "profile", "email"}, + SessionTokenExpiry: time.Hour, + Store: db, + SessionStore: sessionStore, + SessionCookieMaxAge: 60, }) c.Assert(err, qt.IsNil) diff --git a/internal/jimmhttp/auth_handler.go b/internal/jimmhttp/auth_handler.go index ac33cc379..43eb15156 100644 --- a/internal/jimmhttp/auth_handler.go +++ b/internal/jimmhttp/auth_handler.go @@ -4,7 +4,6 @@ import ( "context" "net/http" - "github.com/antonlindstrom/pgstore" "github.com/coreos/go-oidc/v3/oidc" "github.com/go-chi/chi/v5" "github.com/juju/zaputil/zapctx" @@ -20,9 +19,7 @@ type OAuthHandler struct { Router *chi.Mux authenticator BrowserOAuthAuthenticator dashboardFinalRedirectURL string - sessionStore *pgstore.PGStore secureCookies bool - cookieExpiry int } // OAuthHandlerParams holds the parameters to configure the OAuthHandler. @@ -34,15 +31,9 @@ type OAuthHandlerParams struct { // upon completing the authorisation code flow. DashboardFinalRedirectURL string - // SessionStore is the cookie session store. - SessionStore *pgstore.PGStore - // SessionCookies determines if HTTPS must be enabled in order for JIMM // to set cookies when creating browser based sessions. SecureCookies bool - - // CookieExpiry is how long the cookie will be valid before expiring in seconds. - CookieExpiry int } // BrowserOAuthAuthenticator handles authorisation code authentication within JIMM @@ -53,6 +44,13 @@ type BrowserOAuthAuthenticator interface { ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) Email(idToken *oidc.IDToken) (string, error) UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error + CreateBrowserSession( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + secureCookies bool, + email string, + ) error } // NewOAuthHandler returns a new OAuth handler. @@ -63,16 +61,11 @@ func NewOAuthHandler(p OAuthHandlerParams) (*OAuthHandler, error) { if p.DashboardFinalRedirectURL == "" { return nil, errors.E("final redirect url not specified") } - if p.SessionStore == nil { - return nil, errors.E("nil session store") - } return &OAuthHandler{ Router: chi.NewRouter(), authenticator: p.Authenticator, dashboardFinalRedirectURL: p.DashboardFinalRedirectURL, - sessionStore: p.SessionStore, secureCookies: p.SecureCookies, - cookieExpiry: p.CookieExpiry, }, nil } @@ -129,22 +122,16 @@ func (oah *OAuthHandler) Callback(w http.ResponseWriter, r *http.Request) { return } - // If the session is empty, it'll just be an empty session, we only check - // errors for bad decoding etc. - session, err := oah.sessionStore.Get(r, "jimm-browser-session") - if err != nil { - writeError(ctx, w, http.StatusBadRequest, err, "failed to get session") + if err := oah.authenticator.CreateBrowserSession( + ctx, + w, + r, + oah.secureCookies, + email, + ); err != nil { + writeError(ctx, w, http.StatusBadRequest, err, "failed to setup session") } - session.IsNew = true // Sets cookie to a fresh new cookie - session.Options.MaxAge = oah.cookieExpiry // Expiry in seconds - session.Options.Secure = oah.secureCookies // Ensures only sent with HTTPS - session.Options.HttpOnly = false // Allow Javascript to read it - - session.Values["jimm-session"] = email - if err = session.Save(r, w); err != nil { - writeError(ctx, w, http.StatusBadRequest, err, "failed to save session") - } http.Redirect(w, r, oah.dashboardFinalRedirectURL, http.StatusPermanentRedirect) } diff --git a/internal/jimmhttp/auth_handler_test.go b/internal/jimmhttp/auth_handler_test.go index 81cdc95ef..190da8701 100644 --- a/internal/jimmhttp/auth_handler_test.go +++ b/internal/jimmhttp/auth_handler_test.go @@ -2,28 +2,20 @@ package jimmhttp_test import ( "context" - "fmt" "io" - "net" "net/http" - "net/http/cookiejar" - "net/http/httptest" - "net/url" - "regexp" "testing" "time" "github.com/antonlindstrom/pgstore" - "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" + "github.com/gorilla/sessions" - "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/db" - "github.com/canonical/jimm/internal/jimmhttp" "github.com/canonical/jimm/internal/jimmtest" ) -func setupDbAndSessionStore(c *qt.C) (*db.Database, *pgstore.PGStore) { +func setupDbAndSessionStore(c *qt.C) (*db.Database, sessions.Store) { // Setup db ahead of time so we have access to session store db := &db.Database{ DB: jimmtest.PostgresDB(c, func() time.Time { return time.Now() }), @@ -39,49 +31,6 @@ func setupDbAndSessionStore(c *qt.C) (*db.Database, *pgstore.PGStore) { return db, store } -func setupTestServer(c *qt.C, dashboardURL string, db *db.Database, sessionStore *pgstore.PGStore) *httptest.Server { - // Find a random free TCP port. - listener, err := net.Listen("tcp", "127.0.0.1:0") - c.Assert(err, qt.IsNil) - port := fmt.Sprintf("%d", listener.Addr().(*net.TCPAddr).Port) - - // Create unstarted server to enable auth service - s := httptest.NewUnstartedServer(nil) - s.Listener = listener - - // Remember redirect url to check it matches after test server starts - redirectURL := "http://127.0.0.1:" + port + "/callback" - authSvc, err := auth.NewAuthenticationService(context.Background(), auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Hour, - // Now we know the port the test server is running on - RedirectURL: redirectURL, - Store: db, - }) - c.Assert(err, qt.IsNil) - - h, err := jimmhttp.NewOAuthHandler(jimmhttp.OAuthHandlerParams{ - Authenticator: authSvc, - DashboardFinalRedirectURL: dashboardURL, - SessionStore: sessionStore, - SecureCookies: false, - CookieExpiry: 86400, - }) - c.Assert(err, qt.IsNil) - - s.Config.Handler = h.Routes() - - s.Start() - - // Ensure redirectURL is matching port on listener - c.Assert(s.URL+"/callback", qt.Equals, redirectURL) - - return s -} - // TestBrowserAuth goes through the flow of a browser logging in, simulating // the cookie state and handling the callbacks are as expected. Additionally handling // the final callback to the dashboard emulating an endpoint. See setupTestServer @@ -91,72 +40,17 @@ func TestBrowserAuth(t *testing.T) { c := qt.New(t) db, sessionStore := setupDbAndSessionStore(c) - - // Setup final test redirect url server, to emulate - // the dashboard receiving the final piece of the flow - dashboardResponse := "dashboard received final callback" - dashboard := httptest.NewServer( - http.HandlerFunc( - func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, dashboardResponse) - sessionCookie, _ := r.Cookie("jimm-browser-session") - c.Assert(sessionCookie.Name, qt.Equals, "jimm-browser-session") - c.Assert(sessionCookie.Value, qt.Not(qt.Equals), "") - // Check the session exist in db - session, err := sessionStore.Get(r, "jimm-browser-session") - c.Assert(err, qt.IsNil) - c.Assert(session.Values["jimm-session"], qt.Equals, "jimm-test@canonical.com") - }, - ), - ) - defer dashboard.Close() - - s := setupTestServer(c, dashboard.URL, db, sessionStore) - defer s.Close() - - jar, err := cookiejar.New(nil) - c.Assert(err, qt.IsNil) - - client := &http.Client{ - Jar: jar, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - fmt.Println("redirected to", req.URL) - return nil - }, - } - - res, err := client.Get(s.URL + "/login") - c.Assert(err, qt.IsNil) - c.Assert(res.StatusCode, qt.Equals, http.StatusOK) - - defer res.Body.Close() - b, err := io.ReadAll(res.Body) + cookie, err := jimmtest.RunBrowserLogin(db, sessionStore) c.Assert(err, qt.IsNil) - - re := regexp.MustCompile(`action="(.*?)" method=`) - match := re.FindStringSubmatch(string(b)) - loginFormUrl := match[1] - - v := url.Values{} - v.Add("username", "jimm-test") - v.Add("password", "password") - loginResp, err := client.PostForm(loginFormUrl, v) - c.Assert(err, qt.IsNil) - - b, err = io.ReadAll(loginResp.Body) - c.Assert(err, qt.IsNil) - - c.Assert(string(b), qt.Equals, dashboardResponse) - c.Assert(loginResp.StatusCode, qt.Equals, 200) - - defer loginResp.Body.Close() + c.Assert(cookie, qt.Not(qt.Equals), "") } func TestCallbackFailsNoCodePresent(t *testing.T) { c := qt.New(t) db, sessionStore := setupDbAndSessionStore(c) - s := setupTestServer(c, "", db, sessionStore) + s, err := jimmtest.SetupTestDashboardCallbackHandler("", db, sessionStore) + c.Assert(err, qt.IsNil) defer s.Close() // Test with no code present at all @@ -174,7 +68,8 @@ func TestCallbackFailsExchange(t *testing.T) { c := qt.New(t) db, sessionStore := setupDbAndSessionStore(c) - s := setupTestServer(c, "", db, sessionStore) + s, err := jimmtest.SetupTestDashboardCallbackHandler("", db, sessionStore) + c.Assert(err, qt.IsNil) defer s.Close() // Test with no code present at all diff --git a/internal/jimmhttp/websocket.go b/internal/jimmhttp/websocket.go index 178e22494..821056130 100644 --- a/internal/jimmhttp/websocket.go +++ b/internal/jimmhttp/websocket.go @@ -12,6 +12,8 @@ import ( "github.com/juju/zaputil/zapctx" "go.uber.org/zap" + "github.com/canonical/jimm/internal/auth" + "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/servermon" ) @@ -33,6 +35,17 @@ type WSHandler struct { // been started. func (h *WSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctx := req.Context() + var authErr error + + if h.Server != nil && h.Server.GetAuthenticationService() != nil { + ctx, authErr = handleBrowserAuthentication( + ctx, + h.Server.GetAuthenticationService(), + w, + req, + ) + } + ctx = context.WithValue(ctx, contextPathKey("path"), req.URL.EscapedPath()) conn, err := h.Upgrader.Upgrade(w, req, nil) if err != nil { @@ -41,28 +54,85 @@ func (h *WSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { zapctx.Error(ctx, "cannot upgrade websocket", zap.Error(err)) return } + servermon.ConcurrentWebsocketConnections.Inc() defer conn.Close() defer servermon.ConcurrentWebsocketConnections.Dec() defer func() { if err := recover(); err != nil { zapctx.Error(ctx, "websocket panic", zap.Any("err", err), zap.Stack("stack")) - data := websocket.FormatCloseMessage(websocket.CloseInternalServerErr, fmt.Sprintf("%v", err)) - if err := conn.WriteControl(websocket.CloseMessage, data, time.Time{}); err != nil { - zapctx.Error(ctx, "cannot write close message", zap.Error(err)) - } + writeInternalServerErrorClosure(ctx, conn, err) } }() + + if authErr != nil { + zapctx.Error(ctx, "browser authentication error", zap.Any("err", authErr), zap.Stack("stack")) + writeInternalServerErrorClosure(ctx, conn, authErr) + return + } + if h.Server == nil { - data := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") - if err := conn.WriteControl(websocket.CloseMessage, data, time.Time{}); err != nil { - zapctx.Error(ctx, "cannot write close message", zap.Error(err)) - } + writeNormalClosure(ctx, conn) return } + h.Server.ServeWS(ctx, conn) } +func writeNormalClosure(ctx context.Context, conn *websocket.Conn) { + data := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") + if err := conn.WriteControl(websocket.CloseMessage, data, time.Time{}); err != nil { + zapctx.Error(ctx, "cannot write close message", zap.Error(err)) + } +} + +func writeInternalServerErrorClosure(ctx context.Context, conn *websocket.Conn, err any) { + data := websocket.FormatCloseMessage(websocket.CloseInternalServerErr, fmt.Sprintf("%v", err)) + if err := conn.WriteControl(websocket.CloseMessage, data, time.Time{}); err != nil { + zapctx.Error(ctx, "cannot write close message", zap.Error(err)) + } +} + +// handleBrowserAuthentication handles browser authentication when a session cookie +// is present, ultimately placing the identity resolved from the cookie within the +// passed context. +// +// It updates the response header on authentication errors with a InternalServerError, +// and as such is safe to return from your handler upon error without updating +// the response statuses. +func handleBrowserAuthentication(ctx context.Context, authSvc jimm.OAuthAuthenticator, w http.ResponseWriter, req *http.Request) (context.Context, error) { + // We perform cookie authentication at the HTTP layer instead of WS + // due to limitations of setting and retrieving cookies in the WS layer. + // + // If no cookie is present, we expect 1 of 3 scenarios: + // 1. It's a device session token login. + // 2. It's a client credential login. + // 3. It's an "expired" cookie login, and as such no cookie + // has been sent with the request. The handling of this is within + // LoginWithSessionCookie, in which, due to no identityId being present + // we know the cookie expired or a request with no cookie was made. + _, err := req.Cookie(auth.SessionName) + + // Now we know a cookie is present, so let's try perform a cookie login / logic + // as presumably a cookie of this name should only ever be present in the case + // the browser performs a connection. + if err == nil { + ctx, err = authSvc.AuthenticateBrowserSession( + ctx, w, req, + ) + if err != nil { + zapctx.Error(ctx, "authenticate browser session failed", zap.Error(err)) + // Something went wrong when trying to perform the authentication + // of the cookie. + return ctx, err + } + } + + // If there's an error due to failure to find the cookie, just return the context + // and move on presuming it's a device or client credentials login. + return ctx, nil +} + // A WSServer is a websocket server. // // ServeWS should handle all messaging on the websocket connection and @@ -70,4 +140,7 @@ func (h *WSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { // the websocket connection, but not send any control messages. type WSServer interface { ServeWS(context.Context, *websocket.Conn) + + // GetAuthenticationService returns JIMM's authentication services. + GetAuthenticationService() jimm.OAuthAuthenticator } diff --git a/internal/jimmhttp/websocket_test.go b/internal/jimmhttp/websocket_test.go index 8542d2483..8079a0b5b 100644 --- a/internal/jimmhttp/websocket_test.go +++ b/internal/jimmhttp/websocket_test.go @@ -4,6 +4,7 @@ package jimmhttp_test import ( "context" + "net/http" "net/http/httptest" "strings" "testing" @@ -12,7 +13,10 @@ import ( qt "github.com/frankban/quicktest" "github.com/gorilla/websocket" + "github.com/canonical/jimm/internal/auth" + "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/jimmhttp" + "github.com/canonical/jimm/internal/jimmtest" ) func TestWSHandler(t *testing.T) { @@ -57,6 +61,11 @@ func (s echoServer) ServeWS(ctx context.Context, conn *websocket.Conn) { } } +// GetAuthenticationService returns JIMM's oauth authentication service. +func (s echoServer) GetAuthenticationService() jimm.OAuthAuthenticator { + return nil +} + func TestWSHandlerPanic(t *testing.T) { c := qt.New(t) @@ -77,6 +86,11 @@ func TestWSHandlerPanic(t *testing.T) { type panicServer struct{} +// GetAuthenticationService returns JIMM's oauth authentication service. +func (s panicServer) GetAuthenticationService() jimm.OAuthAuthenticator { + return nil +} + func (s panicServer) ServeWS(ctx context.Context, conn *websocket.Conn) { panic("test") } @@ -96,3 +110,32 @@ func TestWSHandlerNilServer(t *testing.T) { _, _, err = conn.ReadMessage() c.Assert(err, qt.ErrorMatches, `websocket: close 1000 \(normal\)`) } + +type authFailServer struct{} + +// GetAuthenticationService returns JIMM's oauth authentication service. +func (s authFailServer) GetAuthenticationService() jimm.OAuthAuthenticator { + return jimmtest.NewMockOAuthAuthenticator("") +} + +func (s authFailServer) ServeWS(ctx context.Context, conn *websocket.Conn) {} + +func TestWSHandlerAuthFailsServer(t *testing.T) { + c := qt.New(t) + + hnd := &jimmhttp.WSHandler{ + Server: authFailServer{}, + } + + srv := httptest.NewServer(hnd) + c.Cleanup(srv.Close) + + var d websocket.Dialer + conn, _, err := d.Dial("ws"+strings.TrimPrefix(srv.URL, "http"), http.Header{ + "Cookie": []string{auth.SessionName + "=naughty_cookie"}, + }) + c.Assert(err, qt.IsNil) + + _, _, err = conn.ReadMessage() + c.Assert(err, qt.ErrorMatches, `websocket: close 1011 \(internal server error\): authentication failed`) +} diff --git a/internal/jimmjwx/utils_test.go b/internal/jimmjwx/utils_test.go index 642a776b7..3e31b7537 100644 --- a/internal/jimmjwx/utils_test.go +++ b/internal/jimmjwx/utils_test.go @@ -109,10 +109,11 @@ func setupService(ctx context.Context, c *qt.C) (*jimm.Service, *httptest.Server AuthModel: cofgaParams.AuthModelID, }, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", }) diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index 19212b0f1..b46e1ee08 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -5,16 +5,30 @@ package jimmtest import ( "context" "encoding/base64" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "net/url" + "regexp" + "strconv" "strings" "time" + "github.com/coreos/go-oidc/v3/oidc" + "github.com/gorilla/sessions" "github.com/juju/juju/api" jujuparams "github.com/juju/juju/rpc/params" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/canonical/jimm/internal/auth" + "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/jimm" + "github.com/canonical/jimm/internal/jimmhttp" "github.com/canonical/jimm/internal/openfga" ) @@ -57,6 +71,10 @@ func (m MockOAuthAuthenticator) VerifySessionToken(token string, secretKey strin return auth.VerifySessionToken(token, m.secretKey) } +func (m MockOAuthAuthenticator) AuthenticateBrowserSession(ctx context.Context, w http.ResponseWriter, req *http.Request) (context.Context, error) { + return ctx, errors.New("authentication failed") +} + // NewUserSessionLogin returns a login provider than be used with Juju Dial Opts // to define how login will take place. In this case we login using a session token // that the JIMM server should verify with the same test secret. @@ -86,3 +104,139 @@ func convertUsernameToEmail(username string) string { } return username } + +func SetupTestDashboardCallbackHandler(browserURL string, db *db.Database, sessionStore sessions.Store) (*httptest.Server, error) { + // Find a random free TCP port. + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, err + } + port := strconv.Itoa(listener.Addr().(*net.TCPAddr).Port) + + // Create unstarted server to enable auth service + s := httptest.NewUnstartedServer(nil) + s.Listener = listener + + // Remember redirect url to check it matches after test server starts + redirectURL := "http://127.0.0.1:" + port + "/callback" + authSvc, err := auth.NewAuthenticationService(context.Background(), auth.AuthenticationServiceParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Hour, + // Now we know the port the test server is running on + RedirectURL: redirectURL, + Store: db, + SessionStore: sessionStore, + SessionCookieMaxAge: 60, + }) + if err != nil { + return nil, err + } + + h, err := jimmhttp.NewOAuthHandler(jimmhttp.OAuthHandlerParams{ + Authenticator: authSvc, + DashboardFinalRedirectURL: browserURL, + SecureCookies: false, + }) + if err != nil { + return nil, err + } + + s.Config.Handler = h.Routes() + + s.Start() + + // Ensure redirectURL is matching port on listener + if s.URL+"/callback" != redirectURL { + return s, errors.New("server callback does not match redirectURL") + } + + return s, nil +} + +func RunBrowserLogin(db *db.Database, sessionStore sessions.Store) (string, error) { + var cookieString string + + // Setup final test redirect url server, to emulate + // the dashboard receiving the final piece of the flow + dashboardResponse := "dashboard received final callback" + browser := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + cookieString = r.Header.Get("Cookie") + w.Write([]byte(dashboardResponse)) + }, + ), + ) + defer browser.Close() + + s, err := SetupTestDashboardCallbackHandler(browser.URL, db, sessionStore) + if err != nil { + return cookieString, err + } + defer s.Close() + + jar, err := cookiejar.New(nil) + if err != nil { + return cookieString, err + } + + client := &http.Client{ + Jar: jar, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + fmt.Println("redirected to", req.URL) + return nil + }, + } + + res, err := client.Get(s.URL + "/login") + if err != nil { + return cookieString, err + } + + if res.StatusCode != http.StatusOK { + return cookieString, errors.New("status code not ok") + } + + defer res.Body.Close() + b, err := io.ReadAll(res.Body) + if err != nil { + return cookieString, err + } + + re := regexp.MustCompile(`action="(.*?)" method=`) + match := re.FindStringSubmatch(string(b)) + loginFormUrl := match[1] + + v := url.Values{} + v.Add("username", "jimm-test") + v.Add("password", "password") + loginResp, err := client.PostForm(loginFormUrl, v) + if err != nil { + return cookieString, err + } + + b, err = io.ReadAll(loginResp.Body) + if err != nil { + return cookieString, err + } + + if string(b) != dashboardResponse { + return cookieString, errors.New("dashboard response not equal") + } + if loginResp.StatusCode != http.StatusOK { + return cookieString, errors.New("status code not ok") + } + + loginResp.Body.Close() + return cookieString, nil +} + +func ParseCookies(cookies string) []*http.Cookie { + header := http.Header{} + header.Add("Cookie", cookies) + request := http.Request{Header: header} + return request.Cookies() +} diff --git a/internal/jimmtest/suite.go b/internal/jimmtest/suite.go index ff8a465e1..49d872095 100644 --- a/internal/jimmtest/suite.go +++ b/internal/jimmtest/suite.go @@ -69,10 +69,12 @@ func (s *JIMMSuite) SetUpTest(c *gc.C) { s.OFGAClient, s.COFGAClient, s.COFGAParams, err = SetupTestOFGAClient(c.TestName()) c.Assert(err, gc.IsNil) + pgdb := PostgresDB(GocheckTester{c}, nil) + // Setup OpenFGA. s.JIMM = &jimm.JIMM{ Database: db.Database{ - DB: PostgresDB(GocheckTester{c}, nil), + DB: pgdb, }, CredentialStore: NewInMemoryCredentialStore(), Pubsub: &pubsub.Hub{MaxConcurrency: 10}, diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index e72965137..81a143f4e 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -71,6 +71,45 @@ func (r *controllerRoot) GetDeviceSessionToken(ctx context.Context) (params.GetD return response, nil } +// LoginWithSessionCookie is a facade call which has the cookie intercepted at the http layer, +// in which it is then placed on the controller root under "identityId", this identityId is used +// to perform a user lookup and authorise the login call. +// +// It may be misleading in that it does not interact with cookies at all, but this will only ever +// be successful upon the http layer login being successful. +func (r *controllerRoot) LoginWithSessionCookie(ctx context.Context) (jujuparams.LoginResult, error) { + const op = errors.Op("jujuapi.LoginWithSessionCookie") + + // If no identity ID has come through, then no cookie was present + // and as such authentication has failed. + if r.identityId == "" { + return jujuparams.LoginResult{}, errors.E(op, &auth.AuthenticationError{}) + } + + user, err := r.jimm.GetOpenFGAUserAndAuthorise(ctx, r.identityId) + if err != nil { + return jujuparams.LoginResult{}, errors.E(op, err) + } + + r.mu.Lock() + r.user = user + r.mu.Unlock() + + // Get server version for LoginResult + srvVersion, err := r.jimm.EarliestControllerVersion(ctx) + if err != nil { + return jujuparams.LoginResult{}, errors.E(op, err) + } + + return jujuparams.LoginResult{ + PublicDNSName: r.params.PublicDNSName, + UserInfo: setupAuthUserInfo(ctx, r, user), + ControllerTag: setupControllerTag(r), + Facades: setupFacades(r), + ServerVersion: srvVersion.String(), + }, nil +} + // LoginWithSessionToken handles logging into the JIMM via a session token that JIMM has // minted itself, this session token is simply a JWT containing the users email // at which point the email is used to perform a lookup for the user, authorise diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index 6a704f4ce..239c32f75 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -4,9 +4,11 @@ package jujuapi_test import ( "context" + "crypto/tls" "encoding/base64" "fmt" "io" + "net" "net/http" "net/http/cookiejar" "net/url" @@ -14,14 +16,19 @@ import ( "strings" "time" + "github.com/antonlindstrom/pgstore" "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/jimmtest" + "github.com/gorilla/websocket" "github.com/coreos/go-oidc/v3/oidc" + "github.com/juju/errors" "github.com/juju/juju/api" + "github.com/juju/juju/rpc/jsoncodec" jujuparams "github.com/juju/juju/rpc/params" + "github.com/juju/juju/utils/proxy" "github.com/juju/names/v4" gc "gopkg.in/check.v1" ) @@ -34,15 +41,23 @@ func (s *adminSuite) SetUpTest(c *gc.C) { s.websocketSuite.SetUpTest(c) ctx := context.Background() + sqldb, err := s.JIMM.Database.DB.DB() + c.Assert(err, gc.IsNil) + + sessionStore, err := pgstore.NewPGStoreFromPool(sqldb, []byte("secretsecretdigletts")) + c.Assert(err, gc.IsNil) + // Replace JIMM's mock authenticator with a real one here // for testing the login flows. authSvc, err := auth.NewAuthenticationService(ctx, auth.AuthenticationServiceParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Hour, - Store: &s.JIMM.Database, + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + ClientSecret: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Hour, + Store: &s.JIMM.Database, + SessionStore: sessionStore, + SessionCookieMaxAge: 60, }) c.Assert(err, gc.Equals, nil) s.JIMM.OAuthAuthenticator = authSvc @@ -62,6 +77,80 @@ func (s *adminSuite) TestLoginToController(c *gc.C) { c.Assert(jujuparams.ErrCode(err), gc.Equals, jujuparams.CodeNotImplemented) } +// TestBrowserLogin takes a test user through the flow of logging into jimm +// via the correct facades. All are done in a single test to see the flow end-2-end. +// +// Within the test are clear comments explaining what is happening when and why. +// Please refer to these comments for further details. +// +// We only test happy path here due to having tested edge cases and failure cases +// within the auth service itself such as invalid cookies, expired access tokens and +// missing/expired/revoked refresh tokens. + +func (s *adminSuite) TestBrowserLogin(c *gc.C) { + // The setup runs a browser login with callback, ultimately retrieving + // a logged in user by cookie. + sqldb, err := s.JIMM.DB().DB.DB() + c.Assert(err, gc.IsNil) + + sessionStore, err := pgstore.NewPGStoreFromPool(sqldb, []byte("secretsecretdigletts")) + c.Assert(err, gc.IsNil) + + cookie, err := jimmtest.RunBrowserLogin(s.JIMM.DB(), sessionStore) + c.Assert(err, gc.IsNil) + c.Assert(cookie, gc.Not(gc.Equals), "") + + cookies := jimmtest.ParseCookies(cookie) + c.Assert(cookies, gc.HasLen, 1) + + jar, err := cookiejar.New(nil) + c.Assert(err, gc.IsNil) + + // Now we move this cookie to the JIMM server on the admin suite and + // set the cookie on the jimm test server url so that the cookie can be + // sent on WS calls. + jimmURL, err := url.Parse(s.Server.URL) + c.Assert(err, gc.IsNil) + jar.SetCookies(jimmURL, cookies) + + conn := s.openWithDialWebsocket( + c, + &api.Info{ + SkipLogin: true, + }, + "test", + getDialWebsocketWithCustomCookieJar(jar), + ) + defer conn.Close() + + lr := &jujuparams.LoginResult{} + err = conn.APICall("Admin", 4, "", "LoginWithSessionCookie", nil, lr) + c.Assert(err, gc.IsNil) + + c.Assert(lr.UserInfo.Identity, gc.Equals, "user-jimm-test@canonical.com") + c.Assert(lr.UserInfo.DisplayName, gc.Equals, "jimm-test") +} + +// TestBrowserLoginNoCookie attempts to login without a cookie. +func (s *adminSuite) TestBrowserLoginNoCookie(c *gc.C) { + conn := s.open( + c, + &api.Info{ + SkipLogin: true, + }, + "test", + ) + defer conn.Close() + + lr := &jujuparams.LoginResult{} + err := conn.APICall("Admin", 4, "", "LoginWithSessionCookie", nil, lr) + c.Assert( + err, + gc.ErrorMatches, + "authentication failed", + ) +} + // TestDeviceLogin takes a test user through the flow of logging into jimm // via the correct facades. All are done in a single test to see the flow end-2-end. // @@ -235,3 +324,51 @@ func (s *adminSuite) TestLoginWithClientCredentials(c *gc.C) { }, &loginResult) c.Assert(err, gc.ErrorMatches, `invalid client credentials \(unauthorized access\)`) } + +// getDialWebsocketWithCustomCookieJar is mostly the default dialer configuration exception +// we need a dial websocket for juju containing a custom cookie jar to send cookies to +// a new server url when testing LoginWithSessionCookie. As such this closure simply +// passes the jar through. +func getDialWebsocketWithCustomCookieJar(jar *cookiejar.Jar) func(ctx context.Context, urlStr string, tlsConfig *tls.Config, ipAddr string) (jsoncodec.JSONConn, error) { + // Copied from github.com/juju/juju@v0.0.0-20240304110523-55fb5d03683b/api/apiclient.go + dialWebsocket := func(ctx context.Context, urlStr string, tlsConfig *tls.Config, ipAddr string) (jsoncodec.JSONConn, error) { + url, err := url.Parse(urlStr) + if err != nil { + return nil, errors.Trace(err) + } + + netDialer := net.Dialer{} + dialer := &websocket.Dialer{ + NetDial: func(netw, addr string) (net.Conn, error) { + if addr == url.Host { + addr = ipAddr + } + return netDialer.DialContext(ctx, netw, addr) + }, + Proxy: proxy.DefaultConfig.GetProxy, + HandshakeTimeout: 45 * time.Second, + TLSClientConfig: tlsConfig, + // We update the jar so that the cookies retrieved from RunBrowserLogin + // can be sent in the LoginWithSessionCookie call. + Jar: jar, + } + + c, resp, err := dialer.Dial(urlStr, nil) + if err != nil { + if err == websocket.ErrBadHandshake { + defer resp.Body.Close() + body, readErr := io.ReadAll(resp.Body) + if readErr == nil { + err = errors.Errorf( + "%s (%s)", + strings.TrimSpace(string(body)), + http.StatusText(resp.StatusCode), + ) + } + } + return nil, errors.Trace(err) + } + return jsoncodec.NewWebsocketConn(c), nil + } + return dialWebsocket +} diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index 9bcd708b9..6ba3a55be 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -131,9 +131,12 @@ type controllerRoot struct { // is created per WS, it is EXPECTED that the subsequent call to GetDeviceSessionToken // happens on the SAME websocket. deviceOAuthResponse *oauth2.DeviceAuthResponse + + // identityId is the id of the identity attempting to login via a session cookie. + identityId string } -func newControllerRoot(j JIMM, p Params) *controllerRoot { +func newControllerRoot(j JIMM, p Params, identityId string) *controllerRoot { watcherRegistry := &watcherRegistry{ watchers: make(map[string]*modelSummaryWatcher), } @@ -143,6 +146,7 @@ func newControllerRoot(j JIMM, p Params) *controllerRoot { watchers: watcherRegistry, pingF: func() {}, controllerUUIDMasking: true, + identityId: identityId, } r.AddMethod("Admin", 1, "Login", rpc.Method(unsupportedLogin)) @@ -152,6 +156,7 @@ func newControllerRoot(j JIMM, p Params) *controllerRoot { r.AddMethod("Admin", 4, "LoginDevice", rpc.Method(r.LoginDevice)) r.AddMethod("Admin", 4, "GetDeviceSessionToken", rpc.Method(r.GetDeviceSessionToken)) r.AddMethod("Admin", 4, "LoginWithSessionToken", rpc.Method(r.LoginWithSessionToken)) + r.AddMethod("Admin", 4, "LoginWithSessionCookie", rpc.Method(r.LoginWithSessionCookie)) r.AddMethod("Admin", 4, "LoginWithClientCredentials", rpc.Method(r.LoginWithClientCredentials)) r.AddMethod("Pinger", 1, "Ping", rpc.Method(r.Ping)) return r diff --git a/internal/jujuapi/export_test.go b/internal/jujuapi/export_test.go index e5ac3a80c..754435102 100644 --- a/internal/jujuapi/export_test.go +++ b/internal/jujuapi/export_test.go @@ -46,7 +46,7 @@ func ToJAASTag(db db.Database, tag *ofganames.Tag) (string, error) { } func NewControllerRoot(j JIMM, p Params) *controllerRoot { - return newControllerRoot(j, p) + return newControllerRoot(j, p, "") } func (r *controllerRoot) GetServiceAccount(ctx context.Context, clientID string) (*openfga.User, error) { diff --git a/internal/jujuapi/pinger_internal_test.go b/internal/jujuapi/pinger_internal_test.go index 5495156dd..8fbe84ece 100644 --- a/internal/jujuapi/pinger_internal_test.go +++ b/internal/jujuapi/pinger_internal_test.go @@ -14,7 +14,7 @@ import ( func TestControllerPing(t *testing.T) { c := qt.New(t) - r := newControllerRoot(nil, Params{}) + r := newControllerRoot(nil, Params{}, "") defer r.cleanup() var calls uint32 r.setPingF(func() { atomic.AddUint32(&calls, 1) }) diff --git a/internal/jujuapi/websocket.go b/internal/jujuapi/websocket.go index fd2fc9a84..823d7163f 100644 --- a/internal/jujuapi/websocket.go +++ b/internal/jujuapi/websocket.go @@ -17,6 +17,7 @@ import ( "github.com/juju/zaputil/zapctx" "go.uber.org/zap" + "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/jimm" @@ -43,9 +44,15 @@ type apiServer struct { params Params } +// GetAuthenticationService returns JIMM's oauth authentication service. +func (s *apiServer) GetAuthenticationService() jimm.OAuthAuthenticator { + return s.jimm.OAuthAuthenticator +} + // ServeWS implements jimmhttp.WSServer. -func (s *apiServer) ServeWS(_ context.Context, conn *websocket.Conn) { - controllerRoot := newControllerRoot(s.jimm, s.params) +func (s *apiServer) ServeWS(ctx context.Context, conn *websocket.Conn) { + identityId := auth.SessionIdentityFromContext(ctx) + controllerRoot := newControllerRoot(s.jimm, s.params, identityId) s.cleanup = controllerRoot.cleanup Dblogger := controllerRoot.newAuditLogger() serveRoot(context.Background(), controllerRoot, Dblogger, conn) @@ -128,6 +135,11 @@ func modelInfoFromPath(path string) (uuid string, finalPath string, err error) { return matches[modelIndex], matches[finalPathIndex], nil } +// GetAuthenticationService returns JIMM's oauth authentication service. +func (s modelProxyServer) GetAuthenticationService() jimm.OAuthAuthenticator { + return s.jimm.OAuthAuthenticator +} + // ServeWS implements jimmhttp.WSServer. func (s modelProxyServer) ServeWS(ctx context.Context, clientConn *websocket.Conn) { jwtGenerator := jimm.NewJWTGenerator(&s.jimm.Database, s.jimm, s.jimm.JWTService) diff --git a/internal/jujuapi/websocket_test.go b/internal/jujuapi/websocket_test.go index e0fb1bb24..833d8fd39 100644 --- a/internal/jujuapi/websocket_test.go +++ b/internal/jujuapi/websocket_test.go @@ -5,6 +5,7 @@ package jujuapi_test import ( "bytes" "context" + "crypto/tls" "encoding/pem" "fmt" "net/http" @@ -12,6 +13,7 @@ import ( "net/url" "github.com/juju/juju/api" + "github.com/juju/juju/rpc/jsoncodec" jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" gc "gopkg.in/check.v1" @@ -99,7 +101,12 @@ func (s *websocketSuite) TearDownTest(c *gc.C) { // openNoAssert creates a new websocket connection to the test server, using the // connection info specified in info, authenticating as the given user. // If info is nil then default values will be used. -func (s *websocketSuite) openNoAssert(c *gc.C, info *api.Info, username string) (api.Connection, error) { +func (s *websocketSuite) openNoAssert( + c *gc.C, + info *api.Info, + username string, + dialWebsocket func(ctx context.Context, urlStr string, tlsConfig *tls.Config, ipAddr string) (jsoncodec.JSONConn, error), +) (api.Connection, error) { var inf api.Info if info != nil { inf = *info @@ -119,14 +126,31 @@ func (s *websocketSuite) openNoAssert(c *gc.C, info *api.Info, username string) lp := jimmtest.NewUserSessionLogin(c, username) - return api.Open(&inf, api.DialOpts{ + dialOpts := api.DialOpts{ InsecureSkipVerify: true, LoginProvider: lp, - }) + } + + if dialWebsocket != nil { + dialOpts.DialWebsocket = dialWebsocket + } + + return api.Open(&inf, dialOpts) } func (s *websocketSuite) open(c *gc.C, info *api.Info, username string) api.Connection { - conn, err := s.openNoAssert(c, info, username) + conn, err := s.openNoAssert(c, info, username, nil) + c.Assert(err, gc.Equals, nil) + return conn +} + +func (s *websocketSuite) openWithDialWebsocket( + c *gc.C, + info *api.Info, + username string, + dialWebsocket func(ctx context.Context, urlStr string, tlsConfig *tls.Config, ipAddr string) (jsoncodec.JSONConn, error), +) api.Connection { + conn, err := s.openNoAssert(c, info, username, dialWebsocket) c.Assert(err, gc.Equals, nil) return conn } diff --git a/service.go b/service.go index 72936970c..29d88115f 100644 --- a/service.go +++ b/service.go @@ -75,6 +75,8 @@ type OAuthAuthenticatorParams struct { // SessionTokenExpiry holds the expiry duration for issued JWTs // for user (CLI) to JIMM authentication. SessionTokenExpiry time.Duration + // SessionCookieMaxAge holds the max age for session cookies. + SessionCookieMaxAge int } // A Params structure contains the parameters required to initialise a new @@ -168,9 +170,6 @@ type Params struct { // SecureSessionCookies determines if HTTPS must be enabled in order for JIMM // to set cookies when creating browser based sessions. SecureSessionCookies bool - - // SessionCookieExpiry is how long the cookie will be valid before expiring in seconds. - SessionCookieExpiry int } // A Service is the implementation of a JIMM server. @@ -266,7 +265,6 @@ func NewService(ctx context.Context, p Params) (*Service, error) { // Cleanup expired session every 30 minutes defer sessionStore.StopCleanup(sessionStore.Cleanup(time.Minute * 30)) - s.jimm.CookieSessionStore = sessionStore if p.AuditLogRetentionPeriodInDays != "" { period, err := strconv.Atoi(p.AuditLogRetentionPeriodInDays) @@ -293,12 +291,14 @@ func NewService(ctx context.Context, p Params) (*Service, error) { authSvc, err := auth.NewAuthenticationService( ctx, auth.AuthenticationServiceParams{ - IssuerURL: p.OAuthAuthenticatorParams.IssuerURL, - ClientID: p.OAuthAuthenticatorParams.ClientID, - ClientSecret: p.OAuthAuthenticatorParams.ClientSecret, - Scopes: p.OAuthAuthenticatorParams.Scopes, - SessionTokenExpiry: p.OAuthAuthenticatorParams.SessionTokenExpiry, - Store: &s.jimm.Database, + IssuerURL: p.OAuthAuthenticatorParams.IssuerURL, + ClientID: p.OAuthAuthenticatorParams.ClientID, + ClientSecret: p.OAuthAuthenticatorParams.ClientSecret, + Scopes: p.OAuthAuthenticatorParams.Scopes, + SessionTokenExpiry: p.OAuthAuthenticatorParams.SessionTokenExpiry, + SessionCookieMaxAge: p.OAuthAuthenticatorParams.SessionCookieMaxAge, + Store: &s.jimm.Database, + SessionStore: sessionStore, }, ) s.jimm.OAuthAuthenticator = authSvc @@ -353,9 +353,7 @@ func NewService(ctx context.Context, p Params) (*Service, error) { oauthHandler, err := jimmhttp.NewOAuthHandler(jimmhttp.OAuthHandlerParams{ Authenticator: authSvc, DashboardFinalRedirectURL: p.DashboardFinalRedirectURL, - SessionStore: sessionStore, SecureCookies: p.SecureSessionCookies, - CookieExpiry: p.SessionCookieExpiry, }) if err != nil { return nil, errors.E(op, err, "failed to setup authentication handler") diff --git a/service_test.go b/service_test.go index 17d61bef2..b243fa0c3 100644 --- a/service_test.go +++ b/service_test.go @@ -47,10 +47,11 @@ func TestDefaultService(t *testing.T) { OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), InsecureSecretStorage: true, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", }) @@ -72,10 +73,11 @@ func TestServiceStartsWithoutSecretStore(t *testing.T) { DSN: jimmtest.CreateEmptyDatabase(c), OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", }) @@ -96,10 +98,11 @@ func TestAuthenticator(t *testing.T) { OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), InsecureSecretStorage: true, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", } @@ -169,10 +172,11 @@ func TestVault(t *testing.T) { VaultSecretFile: "./local/vault/approle.json", OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", } @@ -241,10 +245,11 @@ func TestPostgresSecretStore(t *testing.T) { OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), InsecureSecretStorage: true, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", } @@ -266,10 +271,11 @@ func TestOpenFGA(t *testing.T) { ControllerAdmins: []string{"alice", "eve"}, InsecureSecretStorage: true, OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", } @@ -326,10 +332,11 @@ func TestPublicKey(t *testing.T) { PrivateKey: "c1VkV05+iWzCxMwMVcWbr0YJWQSEO62v+z3EQ2BhFMw=", PublicKey: "pC8MEk9MS9S8fhyRnOJ4qARTcTAwoM9L1nH/Yq0MwWU=", OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", } @@ -414,10 +421,11 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { PrivateKey: "c1VkV05+iWzCxMwMVcWbr0YJWQSEO62v+z3EQ2BhFMw=", PublicKey: "pC8MEk9MS9S8fhyRnOJ4qARTcTAwoM9L1nH/Yq0MwWU=", OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, }, DashboardFinalRedirectURL: "", } From b75255cdd318a97f349cfd168a06f97ac1490be0 Mon Sep 17 00:00:00 2001 From: alesstimec Date: Fri, 22 Mar 2024 08:53:35 +0100 Subject: [PATCH 093/126] Model proxy changes for LoginWithSessionCookie. --- internal/jujuapi/websocket.go | 11 +++-- internal/rpc/proxy.go | 86 +++++++++++++++++++++-------------- internal/rpc/proxy_test.go | 34 ++++++++++++++ 3 files changed, 93 insertions(+), 38 deletions(-) diff --git a/internal/jujuapi/websocket.go b/internal/jujuapi/websocket.go index 823d7163f..9d4bbae18 100644 --- a/internal/jujuapi/websocket.go +++ b/internal/jujuapi/websocket.go @@ -147,11 +147,12 @@ func (s modelProxyServer) ServeWS(ctx context.Context, clientConn *websocket.Con zapctx.Debug(ctx, "Starting proxier") auditLogger := s.jimm.AddAuditLogEntry proxyHelpers := jimmRPC.ProxyHelpers{ - ConnClient: clientConn, - TokenGen: &jwtGenerator, - ConnectController: connectionFunc, - AuditLog: auditLogger, - JIMM: s.jimm, + ConnClient: clientConn, + TokenGen: &jwtGenerator, + ConnectController: connectionFunc, + AuditLog: auditLogger, + JIMM: s.jimm, + AuthenticatedIdentityID: auth.SessionIdentityFromContext(ctx), } jimmRPC.ProxySockets(ctx, proxyHelpers) } diff --git a/internal/rpc/proxy.go b/internal/rpc/proxy.go index b08a640b3..e016b0f30 100644 --- a/internal/rpc/proxy.go +++ b/internal/rpc/proxy.go @@ -59,11 +59,12 @@ type JIMM interface { // ProxyHelpers contains all the necessary helpers for proxying a Juju client // connection to a model. type ProxyHelpers struct { - ConnClient WebsocketConnection - TokenGen TokenGenerator - ConnectController func(context.Context) (WebsocketConnection, string, error) - AuditLog func(*dbmodel.AuditLogEntry) - JIMM JIMM + ConnClient WebsocketConnection + TokenGen TokenGenerator + ConnectController func(context.Context) (WebsocketConnection, string, error) + AuditLog func(*dbmodel.AuditLogEntry) + JIMM JIMM + AuthenticatedIdentityID string } // ProxySockets will proxy requests from a client connection through to a controller @@ -86,12 +87,13 @@ func ProxySockets(ctx context.Context, helpers ProxyHelpers) error { // after the first message has been received so that any errors can be properly sent back to the client. clProxy := clientProxy{ modelProxy: modelProxy{ - src: &client, - msgs: &msgInFlight, - tokenGen: helpers.TokenGen, - auditLog: helpers.AuditLog, - conversationId: utils.NewConversationID(), - jimm: helpers.JIMM, + src: &client, + msgs: &msgInFlight, + tokenGen: helpers.TokenGen, + auditLog: helpers.AuditLog, + conversationId: utils.NewConversationID(), + jimm: helpers.JIMM, + authenticatedIdentityID: helpers.AuthenticatedIdentityID, }, errChan: errChan, createControllerConn: helpers.ConnectController, @@ -192,14 +194,15 @@ func (msgs *inflightMsgs) getMessage(key uint64) *message { } type modelProxy struct { - src *writeLockConn - dst *writeLockConn - msgs *inflightMsgs - auditLog func(*dbmodel.AuditLogEntry) - tokenGen TokenGenerator - jimm JIMM - modelName string - conversationId string + src *writeLockConn + dst *writeLockConn + msgs *inflightMsgs + auditLog func(*dbmodel.AuditLogEntry) + tokenGen TokenGenerator + jimm JIMM + modelName string + conversationId string + authenticatedIdentityID string deviceOAuthResponse *oauth2.DeviceAuthResponse } @@ -553,6 +556,14 @@ func (p *clientProxy) handleAdminFacade(ctx context.Context, msg *message) (clie errorFnc := func(err error) (*message, *message, error) { return nil, nil, err } + controllerLoginMessageFnc := func(data []byte) (*message, *message, error) { + m := *msg + m.Type = "Admin" + m.Request = "Login" + m.Version = 3 + m.Params = data + return nil, &m, nil + } switch msg.Request { case "LoginDevice": deviceResponse, err := jimm.LoginDevice(ctx, p.jimm.OAuthAuthenticationService()) @@ -615,12 +626,7 @@ func (p *clientProxy) handleAdminFacade(ctx context.Context, msg *message) (clie if err != nil { return errorFnc(err) } - m := *msg - m.Type = "Admin" - m.Request = "Login" - m.Version = 3 - m.Params = data - return nil, &m, nil + return controllerLoginMessageFnc(data) case "LoginWithClientCredentials": var request apiparams.LoginWithClientCredentialsRequest err := json.Unmarshal(msg.Params, &request) @@ -648,14 +654,28 @@ func (p *clientProxy) handleAdminFacade(ctx context.Context, msg *message) (clie if err != nil { return errorFnc(err) } - m := *msg - m.Type = "Admin" - m.Request = "Login" - m.Version = 3 - m.Params = data - return nil, &m, nil - case "LoginWithCookie": - return errorFnc(errors.E(errors.CodeNotImplemented)) + return controllerLoginMessageFnc(data) + case "LoginWithSessionCookie": + if p.modelProxy.authenticatedIdentityID == "" { + return errorFnc(errors.E(errors.CodeUnauthorized)) + } + user, err := p.jimm.GetOpenFGAUserAndAuthorise(ctx, p.modelProxy.authenticatedIdentityID) + if err != nil { + return errorFnc(err) + } + + jwt, err := p.tokenGen.MakeLoginToken(ctx, user) + if err != nil { + return errorFnc(err) + } + data, err := json.Marshal(params.LoginRequest{ + AuthTag: user.ResourceTag().String(), + Token: base64.StdEncoding.EncodeToString(jwt), + }) + if err != nil { + return errorFnc(err) + } + return controllerLoginMessageFnc(data) case "Login": return errorFnc(errors.E("JIMM does not support login from old clients", errors.CodeNotSupported)) default: diff --git a/internal/rpc/proxy_test.go b/internal/rpc/proxy_test.go index 8b26cc8e8..24338df48 100644 --- a/internal/rpc/proxy_test.go +++ b/internal/rpc/proxy_test.go @@ -68,6 +68,7 @@ func TestProxySocketsAdminFacade(t *testing.T) { tests := []struct { about string messageToSend message + authenticateEntityID string expectedClientResponse *message expectedControllerMessage *message oauthAuthenticatorError error @@ -199,6 +200,38 @@ func TestProxySocketsAdminFacade(t *testing.T) { Request: "AnyMethod", Params: []byte(`{"key":"value"}`), }, + }, { + about: "login with session cookie - a login message is sent to the controller", + messageToSend: message{ + RequestID: 1, + Type: "Admin", + Version: 4, + Request: "LoginWithSessionCookie", + Params: ccData, + }, + authenticateEntityID: "alice@wonderland.io", + expectedControllerMessage: &message{ + RequestID: 1, + Type: "Admin", + Version: 3, + Request: "Login", + Params: loginData, + }, + }, { + about: "login with session cookie - but there was no identity id in the cookie", + messageToSend: message{ + RequestID: 1, + Type: "Admin", + Version: 4, + Request: "LoginWithSessionCookie", + Params: ccData, + }, + expectedClientResponse: &message{ + RequestID: 1, + Error: "unauthorized access", + ErrorCode: "unauthorized access", + }, + oauthAuthenticatorError: errors.E(errors.CodeUnauthorized), }} for _, test := range tests { @@ -223,6 +256,7 @@ func TestProxySocketsAdminFacade(t *testing.T) { JIMM: &mockJIMM{ authenticator: authenticator, }, + AuthenticatedIdentityID: test.authenticateEntityID, } go rpc.ProxySockets(ctx, helpers) From f579e7b5d0e2ae2d3b173db86e3d0e42701d5667 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:27:45 +0000 Subject: [PATCH 094/126] Css 6659/implement logout endpoint (#1179) Logout endpoint --- internal/auth/oauth2.go | 46 ++++++++++++++++++++++++++ internal/auth/oauth2_test.go | 6 +++- internal/jimmhttp/auth_handler.go | 21 ++++++++++++ internal/jimmhttp/auth_handler_test.go | 42 ++++++++++++++++++++--- internal/jimmtest/auth.go | 46 ++++++++++++++++---------- 5 files changed, 138 insertions(+), 23 deletions(-) diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 3dc47a86a..0df178346 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -439,6 +439,52 @@ func (as *AuthenticationService) AuthenticateBrowserSession(ctx context.Context, return ctx, nil } +// Logout does two things: +// +// - It deletes the session (Max-Age = -1), and within the database the cleanup routine will remove +// the expired session upon next run. +// - It resets the access tokens for this user +func (as *AuthenticationService) Logout(ctx context.Context, w http.ResponseWriter, req *http.Request) error { + const op = errors.Op("auth.AuthenticationService.Logout") + + session, err := as.sessionStore.Get(req, SessionName) + if err != nil { + zapctx.Error(ctx, "failed to retrieve session", zap.Error(err)) + return errors.E(op, err, "failed to retrieve session") + } + + identityId, ok := session.Values[SessionIdentityKey] + if !ok { + err := errors.E(op, "session is missing identity key") + zapctx.Error(ctx, "session is missing identity key", zap.Error(err)) + return err + } + + identityIdStr, ok := identityId.(string) + if !ok { + err := errors.E(op, fmt.Sprintf("session identity key could not be parsed: expected %T, got %T", identityIdStr, identityId)) + zapctx.Error(ctx, "failed to parse session identity key", zap.Error(err)) + return err + } + + if err := as.deleteSession(session, w, req); err != nil { + zapctx.Error(ctx, "failed to delete session", zap.Error(err)) + return errors.E(op, err) + } + + if err := as.UpdateIdentity(ctx, identityIdStr, &oauth2.Token{ + AccessToken: "", + RefreshToken: "", + Expiry: time.Now(), + TokenType: "", + }); err != nil { + zapctx.Error(ctx, "failed to update identity", zap.Error(err)) + return errors.E(op, err) + } + + return nil +} + // validateAndUpdateAccessToken validates the access tokens expiry, and if it cannot, then // it attempts to refresh the access token. func (as *AuthenticationService) validateAndUpdateAccessToken(ctx context.Context, email any) error { diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 0107dce17..0bd3bac8c 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -301,7 +301,7 @@ func TestCreateBrowserSession(t *testing.T) { c.Assert(session.Values[auth.SessionIdentityKey], qt.Equals, "jimm-test@canonical.com") } -func TestAuthenticateBrowserSession(t *testing.T) { +func TestAuthenticateBrowserSessionAndLogout(t *testing.T) { c := qt.New(t) ctx := context.Background() @@ -329,6 +329,10 @@ func TestAuthenticateBrowserSession(t *testing.T) { setCookieCookies := rec.Header().Get("Set-Cookie") parsedCookies := jimmtest.ParseCookies(setCookieCookies) assertSetCookiesIsCorrect(c, rec, parsedCookies) + + // Test logout does indeed remove the cookie for us + err = authSvc.Logout(ctx, rec, req) + c.Assert(err, qt.IsNil) } func TestAuthenticateBrowserSessionRejectsNoneDecryptableOrDecodableCookies(t *testing.T) { diff --git a/internal/jimmhttp/auth_handler.go b/internal/jimmhttp/auth_handler.go index 43eb15156..503fc1d53 100644 --- a/internal/jimmhttp/auth_handler.go +++ b/internal/jimmhttp/auth_handler.go @@ -10,6 +10,7 @@ import ( "go.uber.org/zap" "golang.org/x/oauth2" + "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/errors" ) @@ -51,6 +52,7 @@ type BrowserOAuthAuthenticator interface { secureCookies bool, email string, ) error + Logout(ctx context.Context, w http.ResponseWriter, req *http.Request) error } // NewOAuthHandler returns a new OAuth handler. @@ -74,6 +76,7 @@ func (oah *OAuthHandler) Routes() chi.Router { oah.SetupMiddleware() oah.Router.Get("/login", oah.Login) oah.Router.Get("/callback", oah.Callback) + oah.Router.Get("/logout", oah.Logout) return oah.Router } @@ -135,6 +138,24 @@ func (oah *OAuthHandler) Callback(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, oah.dashboardFinalRedirectURL, http.StatusPermanentRedirect) } +// Logout handles /auth/logout. +func (oah *OAuthHandler) Logout(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + + authSvc := oah.authenticator + + if _, err := r.Cookie(auth.SessionName); err != nil { + writeError(ctx, w, http.StatusForbidden, err, "no session cookie to logout") + return + } + + if err := authSvc.Logout(ctx, w, r); err != nil { + writeError(ctx, w, http.StatusInternalServerError, err, "failed to logout") + return + } + w.WriteHeader(http.StatusOK) +} + // writeError writes an error and logs the message. It is expected that the status code // is an erroneous status code. func writeError(ctx context.Context, w http.ResponseWriter, status int, err error, logMessage string) { diff --git a/internal/jimmhttp/auth_handler_test.go b/internal/jimmhttp/auth_handler_test.go index 190da8701..b58796aa2 100644 --- a/internal/jimmhttp/auth_handler_test.go +++ b/internal/jimmhttp/auth_handler_test.go @@ -31,18 +31,52 @@ func setupDbAndSessionStore(c *qt.C) (*db.Database, sessions.Store) { return db, store } -// TestBrowserAuth goes through the flow of a browser logging in, simulating +// TestBrowserLoginAndLogout goes through the flow of a browser logging in, simulating // the cookie state and handling the callbacks are as expected. Additionally handling -// the final callback to the dashboard emulating an endpoint. See setupTestServer +// the final callback to the dashboard emulating an endpoint. See RunBrowserLogin // where we create an additional handler to simulate the final callback to the dashboard // from JIMM. -func TestBrowserAuth(t *testing.T) { +// +// Finally, it calls the logout using the cookie containing the identity we wish to logout. +func TestBrowserLoginAndLogout(t *testing.T) { c := qt.New(t) + // TODO in WHOAMI PR, run a WHOAMI without cookie + + // Login db, sessionStore := setupDbAndSessionStore(c) - cookie, err := jimmtest.RunBrowserLogin(db, sessionStore) + + pgSession := sessionStore.(*pgstore.PGStore) + pgSession.Cleanup(time.Nanosecond) + + cookie, jimmHTTPServer, err := jimmtest.RunBrowserLoginAndKeepServerRunning(db, sessionStore) c.Assert(err, qt.IsNil) + defer jimmHTTPServer.Close() c.Assert(cookie, qt.Not(qt.Equals), "") + + // TODO in WHOAMI PR, run a WHOAMI with cookie + + // Logout + req, err := http.NewRequest("GET", jimmHTTPServer.URL+"/logout", nil) + c.Assert(err, qt.IsNil) + parsedCookies := jimmtest.ParseCookies(cookie) + c.Assert(parsedCookies, qt.HasLen, 1) + req.AddCookie(parsedCookies[0]) + + res, err := http.DefaultClient.Do(req) + c.Assert(err, qt.IsNil) + c.Assert(res.StatusCode, qt.Equals, http.StatusOK) + + // TODO in WHOAMI PR, run a WHOAMI without cookie + // This is following Kians suggestion of embedding whoami into this test + // which makes 100% sense. + + // Logout with no identity + req, err = http.NewRequest("GET", jimmHTTPServer.URL+"/logout", nil) + c.Assert(err, qt.IsNil) + res, err = http.DefaultClient.Do(req) + c.Assert(err, qt.IsNil) + c.Assert(res.StatusCode, qt.Equals, http.StatusForbidden) } func TestCallbackFailsNoCodePresent(t *testing.T) { diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index b46e1ee08..378b8a3a9 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -156,7 +156,25 @@ func SetupTestDashboardCallbackHandler(browserURL string, db *db.Database, sessi return s, nil } +func RunBrowserLoginAndKeepServerRunning(db *db.Database, sessionStore sessions.Store) (string, *httptest.Server, error) { + cookieString, jimmHTTPServer, err := runBrowserLogin(db, sessionStore) + return cookieString, jimmHTTPServer, err +} + func RunBrowserLogin(db *db.Database, sessionStore sessions.Store) (string, error) { + cookieString, jimmHTTPServer, err := runBrowserLogin(db, sessionStore) + defer jimmHTTPServer.Close() + return cookieString, err +} + +func ParseCookies(cookies string) []*http.Cookie { + header := http.Header{} + header.Add("Cookie", cookies) + request := http.Request{Header: header} + return request.Cookies() +} + +func runBrowserLogin(db *db.Database, sessionStore sessions.Store) (string, *httptest.Server, error) { var cookieString string // Setup final test redirect url server, to emulate @@ -174,13 +192,12 @@ func RunBrowserLogin(db *db.Database, sessionStore sessions.Store) (string, erro s, err := SetupTestDashboardCallbackHandler(browser.URL, db, sessionStore) if err != nil { - return cookieString, err + return cookieString, s, err } - defer s.Close() jar, err := cookiejar.New(nil) if err != nil { - return cookieString, err + return cookieString, s, err } client := &http.Client{ @@ -193,17 +210,17 @@ func RunBrowserLogin(db *db.Database, sessionStore sessions.Store) (string, erro res, err := client.Get(s.URL + "/login") if err != nil { - return cookieString, err + return cookieString, s, err } if res.StatusCode != http.StatusOK { - return cookieString, errors.New("status code not ok") + return cookieString, s, errors.New("status code not ok") } defer res.Body.Close() b, err := io.ReadAll(res.Body) if err != nil { - return cookieString, err + return cookieString, s, err } re := regexp.MustCompile(`action="(.*?)" method=`) @@ -215,28 +232,21 @@ func RunBrowserLogin(db *db.Database, sessionStore sessions.Store) (string, erro v.Add("password", "password") loginResp, err := client.PostForm(loginFormUrl, v) if err != nil { - return cookieString, err + return cookieString, s, err } b, err = io.ReadAll(loginResp.Body) if err != nil { - return cookieString, err + return cookieString, s, err } if string(b) != dashboardResponse { - return cookieString, errors.New("dashboard response not equal") + return cookieString, s, errors.New("dashboard response not equal") } if loginResp.StatusCode != http.StatusOK { - return cookieString, errors.New("status code not ok") + return cookieString, s, errors.New("status code not ok") } loginResp.Body.Close() - return cookieString, nil -} - -func ParseCookies(cookies string) []*http.Cookie { - header := http.Header{} - header.Add("Cookie", cookies) - request := http.Request{Header: header} - return request.Cookies() + return cookieString, s, nil } From ede3ac94638136025a1c7a8921f3cdfd0dc3ac25 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:04:24 +0000 Subject: [PATCH 095/126] Css 6648/implement whoami endpoint (#1180) --- api/params/params.go | 6 ++++ internal/auth/oauth2.go | 41 +++++++++++++++++++--- internal/auth/oauth2_test.go | 16 ++++++--- internal/jimmhttp/auth_handler.go | 46 ++++++++++++++++++++++-- internal/jimmhttp/auth_handler_test.go | 48 ++++++++++++++++++++------ service.go | 2 +- 6 files changed, 137 insertions(+), 22 deletions(-) diff --git a/api/params/params.go b/api/params/params.go index d63bbb132..00827cac3 100644 --- a/api/params/params.go +++ b/api/params/params.go @@ -468,3 +468,9 @@ type GrantServiceAccountAccess struct { // ClientID holds the client id of the service account. ClientID string `json:"client-id"` } + +// WhoamiResponse holds the response for a /auth/whoami call. +type WhoamiResponse struct { + DisplayName string `json:"display-name"` + Email string `json:"email"` +} diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 0df178346..e5c9901a1 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -27,6 +27,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" + "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" ) @@ -53,7 +54,12 @@ func SessionIdentityFromContext(ctx context.Context) string { if v == nil { return "" } - return v.(string) + s, ok := v.(string) + if !ok { + zapctx.Error(ctx, "failed to retrieve identity string from context", zap.Any("identity", v)) + return "" + } + return s } // AuthenticationService handles authentication within JIMM. @@ -64,8 +70,7 @@ type AuthenticationService struct { provider *oidc.Provider // sessionTokenExpiry holds the expiry time for JIMM minted session tokens (JWTs). sessionTokenExpiry time.Duration - - // sessionCookieMaxAge holds the max age for session cookies. + // sessionCookieMaxAge holds the max age for session cookies in seconds. sessionCookieMaxAge int db IdentityStore @@ -95,7 +100,7 @@ type AuthenticationServiceParams struct { Scopes []string // SessionTokenExpiry holds the expiry time of minted JIMM session tokens (JWTs). SessionTokenExpiry time.Duration - // SessionCookieMaxAge holds the max age for session cookies. + // SessionCookieMaxAge holds the max age for session cookies in seconds. SessionCookieMaxAge int // RedirectURL is the URL for handling the exchange of authorisation // codes into access tokens (and id tokens), for JIMM, this is expected @@ -425,7 +430,7 @@ func (as *AuthenticationService) AuthenticateBrowserSession(ctx context.Context, err = as.validateAndUpdateAccessToken(ctx, identityId) if err != nil { if err := as.deleteSession(session, w, req); err != nil { - return ctx, errors.E(op, err) + return ctx, errors.E(op, err, "failed to delete session after getting an invalid token") } return ctx, errors.E(op, err) } @@ -485,6 +490,32 @@ func (as *AuthenticationService) Logout(ctx context.Context, w http.ResponseWrit return nil } +// Whoami returns "whoami" response, based on the identity id populating the fields +// according to the current database schema for identities. This is likely subject +// to change in the future. +func (as *AuthenticationService) Whoami(ctx context.Context) (*params.WhoamiResponse, error) { + const op = errors.Op("auth.AuthenticationService.Whoami") + + identityId := SessionIdentityFromContext(ctx) + if identityId == "" { + return nil, errors.E(op, "no identity in context") + } + + u := &dbmodel.Identity{ + Name: identityId, + } + + if err := as.db.GetIdentity(ctx, u); err != nil { + return nil, errors.E(op, err) + } + + return ¶ms.WhoamiResponse{ + DisplayName: u.DisplayName, + Email: u.Name, + }, nil + +} + // validateAndUpdateAccessToken validates the access tokens expiry, and if it cannot, then // it attempts to refresh the access token. func (as *AuthenticationService) validateAndUpdateAccessToken(ctx context.Context, email any) error { diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 0bd3bac8c..634ecd882 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -264,9 +264,10 @@ func assertSetCookiesIsCorrect(c *qt.C, rec *httptest.ResponseRecorder, parsedCo for _, v := range cookies { if v.Name == name { found = true + break } } - c.Assert(found, qt.IsTrue) + c.Assert(found, qt.IsTrue, qt.Commentf("cookie data assertion failed")) } assertHasCookie(auth.SessionName, parsedCookies) assertHasCookie("Path", parsedCookies) @@ -321,9 +322,11 @@ func TestAuthenticateBrowserSessionAndLogout(t *testing.T) { ctx, err = authSvc.AuthenticateBrowserSession(ctx, rec, req) c.Assert(err, qt.IsNil) - // Check identity added - identityId := auth.SessionIdentityFromContext(ctx) - c.Assert(identityId, qt.Equals, "jimm-test@canonical.com") + // Test whoami + whoamiResp, err := authSvc.Whoami(ctx) + c.Assert(err, qt.IsNil) + c.Assert(whoamiResp.DisplayName, qt.Equals, "jimm-test") + c.Assert(whoamiResp.Email, qt.Equals, "jimm-test@canonical.com") // Assert Set-Cookie present setCookieCookies := rec.Header().Get("Set-Cookie") @@ -333,6 +336,11 @@ func TestAuthenticateBrowserSessionAndLogout(t *testing.T) { // Test logout does indeed remove the cookie for us err = authSvc.Logout(ctx, rec, req) c.Assert(err, qt.IsNil) + + // Test whoami with empty context (simulating a logged out user) + _, err = authSvc.Whoami(context.Background()) + c.Assert(err, qt.ErrorMatches, "no identity in context") + } func TestAuthenticateBrowserSessionRejectsNoneDecryptableOrDecodableCookies(t *testing.T) { diff --git a/internal/jimmhttp/auth_handler.go b/internal/jimmhttp/auth_handler.go index 503fc1d53..7f37eb33d 100644 --- a/internal/jimmhttp/auth_handler.go +++ b/internal/jimmhttp/auth_handler.go @@ -2,6 +2,7 @@ package jimmhttp import ( "context" + "encoding/json" "net/http" "github.com/coreos/go-oidc/v3/oidc" @@ -10,6 +11,7 @@ import ( "go.uber.org/zap" "golang.org/x/oauth2" + "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/errors" ) @@ -53,6 +55,8 @@ type BrowserOAuthAuthenticator interface { email string, ) error Logout(ctx context.Context, w http.ResponseWriter, req *http.Request) error + AuthenticateBrowserSession(ctx context.Context, w http.ResponseWriter, req *http.Request) (context.Context, error) + Whoami(ctx context.Context) (*params.WhoamiResponse, error) } // NewOAuthHandler returns a new OAuth handler. @@ -77,6 +81,7 @@ func (oah *OAuthHandler) Routes() chi.Router { oah.Router.Get("/login", oah.Login) oah.Router.Get("/callback", oah.Callback) oah.Router.Get("/logout", oah.Logout) + oah.Router.Get("/whoami", oah.Whoami) return oah.Router } @@ -92,7 +97,7 @@ func (oah *OAuthHandler) Login(w http.ResponseWriter, r *http.Request) { // Callback handles /auth/callback. func (oah *OAuthHandler) Callback(w http.ResponseWriter, r *http.Request) { - ctx := context.Background() + ctx := r.Context() code := r.URL.Query().Get("code") if code == "" { @@ -140,7 +145,7 @@ func (oah *OAuthHandler) Callback(w http.ResponseWriter, r *http.Request) { // Logout handles /auth/logout. func (oah *OAuthHandler) Logout(w http.ResponseWriter, r *http.Request) { - ctx := context.Background() + ctx := r.Context() authSvc := oah.authenticator @@ -156,6 +161,43 @@ func (oah *OAuthHandler) Logout(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } +// Whoami handles /auth/whoami. +func (oah *OAuthHandler) Whoami(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + authSvc := oah.authenticator + + if _, err := r.Cookie(auth.SessionName); err != nil { + writeError(ctx, w, http.StatusForbidden, err, "no session cookie to identity user") + return + } + + ctx, err := authSvc.AuthenticateBrowserSession(ctx, w, r) + if err != nil { + writeError(ctx, w, http.StatusInternalServerError, err, "failed to authenticate users session") + return + } + + whoamiResp, err := authSvc.Whoami(ctx) + if err != nil { + writeError(ctx, w, http.StatusInternalServerError, err, "failed to find whoami from identity id") + return + } + + b, err := json.Marshal(whoamiResp) + if err != nil { + writeError(ctx, w, http.StatusInternalServerError, err, "failed to marshal whoami resp") + return + } + + if _, err := w.Write(b); err != nil { + writeError(ctx, w, http.StatusInternalServerError, err, "failed to write response to whoami") + return + } + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) +} + // writeError writes an error and logs the message. It is expected that the status code // is an erroneous status code. func writeError(ctx context.Context, w http.ResponseWriter, status int, err error, logMessage string) { diff --git a/internal/jimmhttp/auth_handler_test.go b/internal/jimmhttp/auth_handler_test.go index b58796aa2..926194b57 100644 --- a/internal/jimmhttp/auth_handler_test.go +++ b/internal/jimmhttp/auth_handler_test.go @@ -11,6 +11,7 @@ import ( qt "github.com/frankban/quicktest" "github.com/gorilla/sessions" + "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/jimmtest" ) @@ -41,8 +42,6 @@ func setupDbAndSessionStore(c *qt.C) (*db.Database, sessions.Store) { func TestBrowserLoginAndLogout(t *testing.T) { c := qt.New(t) - // TODO in WHOAMI PR, run a WHOAMI without cookie - // Login db, sessionStore := setupDbAndSessionStore(c) @@ -54,10 +53,8 @@ func TestBrowserLoginAndLogout(t *testing.T) { defer jimmHTTPServer.Close() c.Assert(cookie, qt.Not(qt.Equals), "") - // TODO in WHOAMI PR, run a WHOAMI with cookie - - // Logout - req, err := http.NewRequest("GET", jimmHTTPServer.URL+"/logout", nil) + // Run a whoami logged in + req, err := http.NewRequest("GET", jimmHTTPServer.URL+"/whoami", nil) c.Assert(err, qt.IsNil) parsedCookies := jimmtest.ParseCookies(cookie) c.Assert(parsedCookies, qt.HasLen, 1) @@ -65,17 +62,48 @@ func TestBrowserLoginAndLogout(t *testing.T) { res, err := http.DefaultClient.Do(req) c.Assert(err, qt.IsNil) + defer res.Body.Close() c.Assert(res.StatusCode, qt.Equals, http.StatusOK) + b, err := io.ReadAll(res.Body) + c.Assert(err, qt.IsNil) + c.Assert(string(b), qt.JSONEquals, ¶ms.WhoamiResponse{ + DisplayName: "jimm-test", + Email: "jimm-test@canonical.com", + }) - // TODO in WHOAMI PR, run a WHOAMI without cookie - // This is following Kians suggestion of embedding whoami into this test - // which makes 100% sense. + // Logout + req, err = http.NewRequest("GET", jimmHTTPServer.URL+"/logout", nil) + c.Assert(err, qt.IsNil) + req.AddCookie(parsedCookies[0]) - // Logout with no identity + res, err = http.DefaultClient.Do(req) + c.Assert(err, qt.IsNil) + defer res.Body.Close() + c.Assert(res.StatusCode, qt.Equals, http.StatusOK) + + // Run a whoami logged out + req, err = http.NewRequest("GET", jimmHTTPServer.URL+"/whoami", nil) + c.Assert(err, qt.IsNil) + parsedCookies = jimmtest.ParseCookies(cookie) + c.Assert(parsedCookies, qt.HasLen, 1) + req.AddCookie(parsedCookies[0]) + + res, err = http.DefaultClient.Do(req) + c.Assert(err, qt.IsNil) + defer res.Body.Close() + c.Assert(res.StatusCode, qt.Equals, http.StatusInternalServerError) + b, err = io.ReadAll(res.Body) + c.Assert(err, qt.IsNil) + // TODO(ale8k): Really it isn't an internal server error here, the session is just + // missing in our store, we should probably bring this error up and return a forbidden. + c.Assert(string(b), qt.Equals, "Internal Server Error") + + // Run a logout with no identity req, err = http.NewRequest("GET", jimmHTTPServer.URL+"/logout", nil) c.Assert(err, qt.IsNil) res, err = http.DefaultClient.Do(req) c.Assert(err, qt.IsNil) + defer res.Body.Close() c.Assert(res.StatusCode, qt.Equals, http.StatusForbidden) } diff --git a/service.go b/service.go index 29d88115f..7746f8978 100644 --- a/service.go +++ b/service.go @@ -75,7 +75,7 @@ type OAuthAuthenticatorParams struct { // SessionTokenExpiry holds the expiry duration for issued JWTs // for user (CLI) to JIMM authentication. SessionTokenExpiry time.Duration - // SessionCookieMaxAge holds the max age for session cookies. + // SessionCookieMaxAge holds the max age for session cookies in seconds. SessionCookieMaxAge int } From 4c4c00497358579de42717796d4a32fd4ddabecb Mon Sep 17 00:00:00 2001 From: alesstimec Date: Tue, 26 Mar 2024 10:18:58 +0100 Subject: [PATCH 096/126] Stores login message separately. --- internal/rpc/proxy.go | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/internal/rpc/proxy.go b/internal/rpc/proxy.go index e016b0f30..12e2ab0f3 100644 --- a/internal/rpc/proxy.go +++ b/internal/rpc/proxy.go @@ -159,22 +159,30 @@ func (c *writeLockConn) sendMessage(responseObject any, request *message) { } type inflightMsgs struct { - mu sync.Mutex - messages map[uint64]*message + mu sync.Mutex + loginMessage *message + messages map[uint64]*message +} + +func (msgs *inflightMsgs) addLoginMessage(msg *message) { + msgs.mu.Lock() + defer msgs.mu.Unlock() + + msgs.loginMessage = msg +} + +func (msgs *inflightMsgs) getLoginMessage() *message { + msgs.mu.Lock() + defer msgs.mu.Unlock() + + return msgs.loginMessage } func (msgs *inflightMsgs) addMessage(msg *message) { msgs.mu.Lock() defer msgs.mu.Unlock() - // Putting the login request on ID 0 to persist it. - // Note (alesstimec) It's a bit confusing that we automagically add "login" message - // as the first message. We should revisit this. - if msg.Type == "Admin" && msg.Request == "Login" { - msgs.messages[0] = msg - } else { - msgs.messages[msg.RequestID] = msg - } + msgs.messages[msg.RequestID] = msg } func (msgs *inflightMsgs) removeMessage(msg *message) { @@ -302,7 +310,7 @@ func (p *clientProxy) start(ctx context.Context) error { // All requests should be proxied as transparently as possible through to the controller // except for auth related requests like Login because JIMM is auth gateway. if msg.Type == "Admin" { - zapctx.Debug(ctx, "Found an Admin facade call") + zapctx.Debug(ctx, "handling an Admin facade call") toClient, toController, err := p.handleAdminFacade(ctx, msg) if err != nil { p.sendError(p.src, msg, err) @@ -312,6 +320,7 @@ func (p *clientProxy) start(ctx context.Context) error { p.src.sendMessage(nil, toClient) } else if toController != nil { msg = toController + p.msgs.addLoginMessage(toController) } } if msg.RequestID == 0 { @@ -475,10 +484,8 @@ func checkPermissionsRequired(ctx context.Context, msg *message) (map[string]any func (p *controllerProxy) redoLogin(ctx context.Context, permissions map[string]any) error { const op = errors.Op("rpc.redoLogin") - var loginMsg *message - if msg, ok := p.msgs.messages[0]; ok { - loginMsg = msg - } + + loginMsg := p.msgs.getLoginMessage() if loginMsg == nil { return errors.E(op, errors.CodeUnauthorized, "Haven't received login yet") } From 88d870000c00a84ecc9932e0aa5c7442482a08c6 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:24:45 +0000 Subject: [PATCH 097/126] Plug redirect url into service.go (#1184) * Plug redirect url into servie.go --- internal/auth/oauth2.go | 6 ++++++ internal/jimmhttp/auth_handler.go | 8 +++++++- local/keycloak/jimm-realm.json | 4 +++- service.go | 14 +++++++++++++- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index e5c9901a1..8217c957a 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -91,17 +91,23 @@ type AuthenticationServiceParams struct { // IssuerURL is the URL of the OAuth2.0 server. // I.e., http://localhost:8082/realms/jimm in the case of keycloak. IssuerURL string + // ClientID holds the OAuth2.0 client id. The client IS expected to be confidential. ClientID string + // ClientSecret holds the OAuth2.0 "client-secret" to authenticate when performing // /auth and /token requests. ClientSecret string + // Scopes holds the scopes that you wish to retrieve. Scopes []string + // SessionTokenExpiry holds the expiry time of minted JIMM session tokens (JWTs). SessionTokenExpiry time.Duration + // SessionCookieMaxAge holds the max age for session cookies in seconds. SessionCookieMaxAge int + // RedirectURL is the URL for handling the exchange of authorisation // codes into access tokens (and id tokens), for JIMM, this is expected // to be the servers own callback endpoint registered under /auth/callback. diff --git a/internal/jimmhttp/auth_handler.go b/internal/jimmhttp/auth_handler.go index 7f37eb33d..c23b22dce 100644 --- a/internal/jimmhttp/auth_handler.go +++ b/internal/jimmhttp/auth_handler.go @@ -16,6 +16,12 @@ import ( "github.com/canonical/jimm/internal/errors" ) +// CallbackEndpoint holds the endpoint path for OAuth2.0 authorisation +// flow callbacks. +const ( + CallbackEndpoint = "/callback" +) + // OAuthHandler handles the oauth2.0 browser flow for JIMM. // Implements jimmhttp.JIMMHttpHandler. type OAuthHandler struct { @@ -79,7 +85,7 @@ func NewOAuthHandler(p OAuthHandlerParams) (*OAuthHandler, error) { func (oah *OAuthHandler) Routes() chi.Router { oah.SetupMiddleware() oah.Router.Get("/login", oah.Login) - oah.Router.Get("/callback", oah.Callback) + oah.Router.Get(CallbackEndpoint, oah.Callback) oah.Router.Get("/logout", oah.Logout) oah.Router.Get("/whoami", oah.Whoami) return oah.Router diff --git a/local/keycloak/jimm-realm.json b/local/keycloak/jimm-realm.json index cf0a1488d..ae1e5b0cf 100644 --- a/local/keycloak/jimm-realm.json +++ b/local/keycloak/jimm-realm.json @@ -806,7 +806,9 @@ "clientAuthenticatorType": "client-secret", "secret": "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4", "redirectUris": [ - "http://127.0.0.1/*" + "http://127.0.0.1/*", + "https://jimm.localhost/*", + "https://localhost/*" ], "webOrigins": [ "*" diff --git a/service.go b/service.go index 7746f8978..9c1d12847 100644 --- a/service.go +++ b/service.go @@ -65,16 +65,21 @@ type OAuthAuthenticatorParams struct { // IssuerURL is the URL of the OAuth2.0 server. // I.e., http://localhost:8082/realms/jimm in the case of keycloak. IssuerURL string + // ClientID holds the OAuth2.0. The client IS expected to be confidential. ClientID string + // ClientSecret holds the OAuth2.0 "client-secret" to authenticate when performing // /auth and /token requests. ClientSecret string + // Scopes holds the scopes that you wish to retrieve. Scopes []string + // SessionTokenExpiry holds the expiry duration for issued JWTs // for user (CLI) to JIMM authentication. SessionTokenExpiry time.Duration + // SessionCookieMaxAge holds the max age for session cookies in seconds. SessionCookieMaxAge int } @@ -288,6 +293,12 @@ func NewService(ctx context.Context, p Params) (*Service, error) { return nil, errors.E(op, err, "failed to ensure controller admins") } + authResourceBasePath := "/auth" + redirectUrl := p.PublicDNSName + authResourceBasePath + jimmhttp.CallbackEndpoint + if !strings.HasPrefix(redirectUrl, "https://") || !strings.HasPrefix(redirectUrl, "http://") { + redirectUrl = "https://" + redirectUrl + } + authSvc, err := auth.NewAuthenticationService( ctx, auth.AuthenticationServiceParams{ @@ -299,6 +310,7 @@ func NewService(ctx context.Context, p Params) (*Service, error) { SessionCookieMaxAge: p.OAuthAuthenticatorParams.SessionCookieMaxAge, Store: &s.jimm.Database, SessionStore: sessionStore, + RedirectURL: redirectUrl, }, ) s.jimm.OAuthAuthenticator = authSvc @@ -359,7 +371,7 @@ func NewService(ctx context.Context, p Params) (*Service, error) { return nil, errors.E(op, err, "failed to setup authentication handler") } mountHandler( - "/auth", + authResourceBasePath, oauthHandler, ) macaroonDischarger, err := s.setupDischarger(p) From ca063e72560891cb0a17cd21acaa20ef5e6cee28 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Wed, 27 Mar 2024 10:04:05 +0000 Subject: [PATCH 098/126] Css 7801/document browser flow (#1183) --- doc/jimm-auth.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 doc/jimm-auth.md diff --git a/doc/jimm-auth.md b/doc/jimm-auth.md new file mode 100644 index 000000000..dab403620 --- /dev/null +++ b/doc/jimm-auth.md @@ -0,0 +1,80 @@ +# OAuth, JIMM and OIDC + + +## Introduction +JIMM has introduced OAuth for federated authentication, i.e., the ability to sign in via an external identity provider. The flow used is [authorisation code](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow). On top of this, JIMM now uses (OpenID Connect)[https://www.microsoft.com/en-us/security/business/security-101/what-is-openid-connect-oidc#:~:text=and%20use%20cases-,OpenID%20Connect%20(OIDC)%20defined,in%20to%20access%20digital%20services.]. + +To perform a login against JIMM using the authorisation code flow from a browser, there are 4 HTTP endpoints available and 1 websocket facade call. + +## Performing a login (HTTP) +### HTTP /auth/login GET +This will perform a a temporary redirect (307) to the /auth endpoint of JAAS' OAuth capable IdP server. The user will then be expected to login using any of the configured methods on the OAuth server, such as social sign in (e.g. Sign in with Google/Github/etc) or self service. + +### HTTP /auth/callback REDIRECT +Upon a successful login, the OAuth server will redirect back to JIMM's callback endpoint. + +This endpoint will do the following: +1. Authenticate the user with the OAuth server +2. Create a session for the user within JIMM's database +3. Create and return an encrypted cookie containing the session information +4. Redirect the user back to a configurable final redirect URL (likely the Juju dashboard) +5. Attempt to extract the email claim from the id_token +6. Create a session within JIMM's internal database and then attach an encrypted cookie containing the session identity ID to the response for the final redirect called "jimm-browser-session", finally, jimm redirect back the the configured final redirect URL (which is likely to be the Juju dashboard) + +> Note: The cookie returned will have HTTP Only set to false, enabling SPA's (Single Page Application) to read the cookie. + +After receiving the redirect from JIMM, the browser will now store the cookie and it can be used for the next steps. + +## Performing authentication (HTTP and WS) +### HTTP /auth/whoami GET +To confirm the identity that has been logged in from the cookie that has been returned in the final callback, the consumer will need to perform a get request to this endpoint. This endpoint will return (when a cookie can successfully be parsed into an application session and it is valid): +```json +{ + "display-name": "", + "email": "" +} +``` + +In addition to this, the whoami endpoint will extend the users session by the configured max age field on the JIMM server, returning an updated cookie. + +If no cookie is provided, a status Forbidden 403 will be returned, informing the consumer that they have no session cookie. + +In the event of an internal server error, a status Internal Server Error 500 will be returned. + +### WS /api and /api/{model id} WS PROTOCOL +The facade details to login are as follows: +- Facade name: `Admin` +- Version: `4 and above` +- Method name: `LoginWithSessionCookie` +- Parameters: `None` + +The cookie header must be present on the initial request to open the websocket and must contain the cookie "jimm-browser-session", which holds the encrypted session identity that was returned in `/auth/callback`. + +## Performing a logout (HTTP) +### /auth/logout GET +To logout, simply hit this endpoint. + +If no cookie is present, a status Forbidden 403 will be returned, informating the consumer that they have no session cookie. + +In the event of an internal server error, a status Internal Server Error 500 will be returned. + +Otherwise, a status OK 200 will be returned, which will reset the cookies max-age to -1, informing the browser to remove the session cookie immediately. + +# Sessions +## The kind of sessions + +### IdP Sessions +The IdP will hold a session for the authenticated user, meaning, that should another OAuth +flow be processed, if the user has already entered their credentials, and a session is active, they will not be expected to enter them again until the IdP session expires. + +This means, should the user be redirected to the IdP's login page, they'll only have to perform a consent and not enter their credentials (if consent is enabled), and then they will be immediately redirected to the configured redirect URI callback. + +### OAuth Sessions +OAuth sessions are often referred to as offline sessions, which directly relates to the use +of the [offline_access](https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess) scope. The access token expiry is used to determine if an OAuth session is currently active, and to be refreshed is handled by the IdP's offline_access idle timeout and/or expiry. If the refresh tokens idle timeout is reached, the token is revoked and any existing access tokens are also revoked. + +For an example of how keycloak handles this, see [here](https://wjw465150.gitbooks.io/keycloak-documentation/content/server_admin/topics/sessions/offline.html). + +### Application Sessions +Application sessions are sessions between the client and the application, they may use means such as a JWT, cookie or some other means to authenticate the user for some period of time. The refreshing of these sessions is dependent on the OAuth session, and whether it is still valid. + From df006a9f0ca6194e588a141aaf183cdc5fd4728e Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Tue, 2 Apr 2024 12:13:28 +0200 Subject: [PATCH 099/126] Update jimm-auth.md (#1186) fixed markdown on link --- doc/jimm-auth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/jimm-auth.md b/doc/jimm-auth.md index dab403620..838cba630 100644 --- a/doc/jimm-auth.md +++ b/doc/jimm-auth.md @@ -2,7 +2,7 @@ ## Introduction -JIMM has introduced OAuth for federated authentication, i.e., the ability to sign in via an external identity provider. The flow used is [authorisation code](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow). On top of this, JIMM now uses (OpenID Connect)[https://www.microsoft.com/en-us/security/business/security-101/what-is-openid-connect-oidc#:~:text=and%20use%20cases-,OpenID%20Connect%20(OIDC)%20defined,in%20to%20access%20digital%20services.]. +JIMM has introduced OAuth for federated authentication, i.e., the ability to sign in via an external identity provider. The flow used is [authorisation code](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow). On top of this, JIMM now uses [OpenID Connect](https://www.microsoft.com/en-us/security/business/security-101/what-is-openid-connect-oidc#:~:text=and%20use%20cases-,OpenID%20Connect%20(OIDC)%20defined,in%20to%20access%20digital%20services.). To perform a login against JIMM using the authorisation code flow from a browser, there are 4 HTTP endpoints available and 1 websocket facade call. From 941c3ccf8dbcb906f074cf726cd67aa028939f59 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:15:34 +0100 Subject: [PATCH 100/126] Update charms for browser flow (#1185) * Update charms for browser flow * new lines * pr comments * pr comments * pr comments * formatting * dd --- charms/jimm-k8s/config.yaml | 17 +++++++++++++++++ charms/jimm-k8s/src/charm.py | 3 +++ charms/jimm-k8s/tests/unit/test_charm.py | 5 +++++ charms/jimm/config.yaml | 17 +++++++++++++++++ charms/jimm/src/charm.py | 3 +++ charms/jimm/templates/jimm.env | 3 +++ charms/jimm/tests/test_charm.py | 23 ++++++++++++++++------- 7 files changed, 64 insertions(+), 7 deletions(-) diff --git a/charms/jimm-k8s/config.yaml b/charms/jimm-k8s/config.yaml index bbf92dde2..72457fc34 100644 --- a/charms/jimm-k8s/config.yaml +++ b/charms/jimm-k8s/config.yaml @@ -100,3 +100,20 @@ options: type: string default: 24h description: Expiry duration for authentication macaroons. + secure-session-cookies: + type: boolean + default: true + description: | + Whether HTTPS must be enabled to set session cookies. + session-cookie-max-age: + type: int + default: 86400 + description: | + The max age for the session cookies in seconds, on subsequent logins, the session instance + extended by this amount. + final-redirect-url: + type: string + default: "" + description: | + The final redirect URL for JIMM to redirect to when completing a browser based + login. This should be your dashboard. diff --git a/charms/jimm-k8s/src/charm.py b/charms/jimm-k8s/src/charm.py index 1734dda29..49c9fe09d 100755 --- a/charms/jimm-k8s/src/charm.py +++ b/charms/jimm-k8s/src/charm.py @@ -292,6 +292,9 @@ def _update_workload(self, event): "JIMM_OAUTH_CLIENT_ID": oauth_provider_info.client_id, "JIMM_OAUTH_CLIENT_SECRET": oauth_provider_info.client_secret, "JIMM_OAUTH_SCOPES": oauth_provider_info.scope, + "JIMM_DASHBOARD_FINAL_REDIRECT_URL:": self.config.get("final-redirect-url"), + "JIMM_SECURE_SESSION_COOKIES:": self.config.get("secure-session-cookies"), + "JIMM_SESSION_COOKIE_MAX_AGE:": self.config.get("session-cookie-max-age"), } if self._state.dsn: config_values["JIMM_DSN"] = self._state.dsn diff --git a/charms/jimm-k8s/tests/unit/test_charm.py b/charms/jimm-k8s/tests/unit/test_charm.py index 3053aa224..f71d397a9 100644 --- a/charms/jimm-k8s/tests/unit/test_charm.py +++ b/charms/jimm-k8s/tests/unit/test_charm.py @@ -33,6 +33,7 @@ "public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", "vault-access-address": "10.0.1.123", + "final-redirect-url": "some-url", } EXPECTED_ENV = { @@ -54,6 +55,9 @@ "JIMM_OAUTH_CLIENT_ID": OAUTH_CLIENT_ID, "JIMM_OAUTH_CLIENT_SECRET": OAUTH_CLIENT_SECRET, "JIMM_OAUTH_SCOPES": OAUTH_PROVIDER_INFO["scope"], + "JIMM_DASHBOARD_FINAL_REDIRECT_URL:": "some-url", + "JIMM_SECURE_SESSION_COOKIES:": True, + "JIMM_SESSION_COOKIE_MAX_AGE:": 86400, } @@ -218,6 +222,7 @@ def test_bakery_configuration(self): "candid-agent-private-key": "test-private-key", "public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", + "final-redirect-url": "some-url", } ) diff --git a/charms/jimm/config.yaml b/charms/jimm/config.yaml index 0b87014de..d09278233 100644 --- a/charms/jimm/config.yaml +++ b/charms/jimm/config.yaml @@ -88,3 +88,20 @@ options: Expiry duration for JIMM session tokens. These tokens are used by clients and their expiry determines how frequently a user must login. + secure-session-cookies: + type: boolean + default: true + description: | + Whether HTTPS must be enabled to set session cookies. + session-cookie-max-age: + type: int + default: 86400 + description: | + The max age for the session cookies in seconds, on subsequent logins, the session instance + extended by this amount. + final-redirect-url: + type: string + default: "" + description: | + The final redirect URL for JIMM to redirect to when completing a browser based + login. This should be your dashboard. diff --git a/charms/jimm/src/charm.py b/charms/jimm/src/charm.py index 0ed65781f..9d170ebd0 100755 --- a/charms/jimm/src/charm.py +++ b/charms/jimm/src/charm.py @@ -153,6 +153,9 @@ def _on_config_changed(self, _): "jwt_expiry": self.config.get("jwt-expiry", "5m"), "macaroon_expiry_duration": self.config.get("macaroon-expiry-duration"), "session_expiry_duration": self.config.get("session-expiry-duration"), + "secure_session_cookies": self.config.get("secure-session-cookies"), + "session_cookie_max_age": self.config.get("session-cookie-max-age"), + "final_redirect_url": self.config.get("final-redirect-url"), } self.oauth.update_client_config(client_config=self._oauth_client_config) diff --git a/charms/jimm/templates/jimm.env b/charms/jimm/templates/jimm.env index 4dc0a4ad5..ab7e19584 100644 --- a/charms/jimm/templates/jimm.env +++ b/charms/jimm/templates/jimm.env @@ -24,3 +24,6 @@ JIMM_JWT_EXPIRY={{jwt_expiry}} {% endif %} JIMM_MACAROON_EXPIRY_DURATION={{macaroon_expiry_duration}} JIMM_ACCESS_TOKEN_EXPIRY_DURATION={{session_expiry_duration}} +JIMM_SECURE_SESSION_COOKIES={{secure_session_cookies}} +JIMM_SESSION_COOKIE_MAX_AGE={{session_cookie_max_age}} +JIMM_DASHBOARD_FINAL_REDIRECT_URL={{final_redirect_url}} diff --git a/charms/jimm/tests/test_charm.py b/charms/jimm/tests/test_charm.py index c4be19d43..e21c539a1 100644 --- a/charms/jimm/tests/test_charm.py +++ b/charms/jimm/tests/test_charm.py @@ -172,13 +172,16 @@ def test_config_changed(self): "audit-log-retention-period-in-days": "10", "jwt-expiry": "10m", "macaroon-expiry-duration": "48h", + "secure-session-cookies": True, + "session-cookie-max-age": 86400, + "final-redirect-url": "", } ) self.assertTrue(os.path.exists(config_file)) with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 22) + self.assertEqual(len(lines), 25) self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=") self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com") self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1") @@ -222,13 +225,16 @@ def test_config_changed_redirect_to_dashboard(self): "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", "audit-log-retention-period-in-days": "10", "macaroon-expiry-duration": "48h", + "secure-session-cookies": True, + "session-cookie-max-age": 86400, + "final-redirect-url": "", } ) self.assertTrue(os.path.exists(config_file)) with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 22) + self.assertEqual(len(lines), 25) self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=") self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com") self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1") @@ -270,13 +276,16 @@ def test_config_changed_ready(self): "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", "audit-log-retention-period-in-days": "10", "macaroon-expiry-duration": "48h", + "secure-session-cookies": True, + "session-cookie-max-age": 86400, + "final-redirect-url": "", } ) self.assertTrue(os.path.exists(config_file)) with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 20) + self.assertEqual(len(lines), 23) self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=") self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com") self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1") @@ -329,7 +338,7 @@ def test_config_changed_with_agent(self): with open(config_file) as f: lines = f.readlines() - self.assertEqual(len(lines), 20) + self.assertEqual(len(lines), 23) self.assertEqual( lines[0].strip(), "BAKERY_AGENT_FILE=" + self.harness.charm._agent_filename, @@ -356,7 +365,7 @@ def test_config_changed_with_agent(self): ) with open(config_file) as f: lines = f.readlines() - self.assertEqual(len(lines), 20) + self.assertEqual(len(lines), 23) self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=") self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com") self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1") @@ -654,14 +663,14 @@ def test_insecure_secret_storage(self): with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 22) + self.assertEqual(len(lines), 25) self.assertEqual(len([match for match in lines if "INSECURE_SECRET_STORAGE" in match]), 0) self.harness.update_config({"postgres-secret-storage": True}) self.assertTrue(os.path.exists(config_file)) with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 24) + self.assertEqual(len(lines), 27) self.assertEqual(len([match for match in lines if "INSECURE_SECRET_STORAGE" in match]), 1) From 0028df21bc9ab70ee0d3c856a0bec738ce3faef6 Mon Sep 17 00:00:00 2001 From: alesstimec Date: Thu, 4 Apr 2024 10:29:21 +0200 Subject: [PATCH 101/126] Various OAuth fixes - fixes the proxy session token verification - changes the format of the client credentials client id bringing it in line with juju user tags - various local testing fixes --- cmd/jaas/cmd/addserviceaccount_test.go | 2 +- cmd/jaas/cmd/grant_test.go | 2 +- .../cmd/listserviceaccountcredentials_test.go | 2 +- cmd/jaas/cmd/updatecredentials_test.go | 18 ++++---- docker-compose.yaml | 9 +++- go.mod | 17 ++++---- go.sum | 41 +++++++++++------- internal/auth/oauth2_test.go | 2 +- internal/jimm/service_account_test.go | 8 ++-- internal/jujuapi/admin_test.go | 4 +- internal/jujuapi/service_account_test.go | 42 +++++++++---------- internal/rpc/proxy.go | 9 ++-- local/jimm/setup-controller.sh | 2 +- local/keycloak/jimm-realm.json | 2 +- local/traefik/traefik.yaml | 13 +++--- pkg/names/service_account.go | 18 ++++---- pkg/names/service_account_test.go | 22 ++++++---- 17 files changed, 118 insertions(+), 95 deletions(-) diff --git a/cmd/jaas/cmd/addserviceaccount_test.go b/cmd/jaas/cmd/addserviceaccount_test.go index 18ed7f18e..565ae5d53 100644 --- a/cmd/jaas/cmd/addserviceaccount_test.go +++ b/cmd/jaas/cmd/addserviceaccount_test.go @@ -24,7 +24,7 @@ type addServiceAccountSuite struct { var _ = gc.Suite(&addServiceAccountSuite{}) func (s *addServiceAccountSuite) TestAddServiceAccount(c *gc.C) { - clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af@canonical.com" // alice is superuser bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err := cmdtesting.RunCommand(c, cmd.NewAddServiceAccountCommandForTesting(s.ClientStore(), bClient), clientID) diff --git a/cmd/jaas/cmd/grant_test.go b/cmd/jaas/cmd/grant_test.go index 042d167cb..7dd1503de 100644 --- a/cmd/jaas/cmd/grant_test.go +++ b/cmd/jaas/cmd/grant_test.go @@ -27,7 +27,7 @@ var _ = gc.Suite(&grantSuite{}) func (s *grantSuite) TestGrant(c *gc.C) { ctx := context.Background() - clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af@canonical.com" // alice is superuser bClient := jimmtest.NewUserSessionLogin(c, "alice") diff --git a/cmd/jaas/cmd/listserviceaccountcredentials_test.go b/cmd/jaas/cmd/listserviceaccountcredentials_test.go index 03bf8184c..0a7c9c4c8 100644 --- a/cmd/jaas/cmd/listserviceaccountcredentials_test.go +++ b/cmd/jaas/cmd/listserviceaccountcredentials_test.go @@ -34,7 +34,7 @@ func (s *listServiceAccountCredentialsSuite) TestListServiceAccountCredentials(c }) c.Assert(err, gc.IsNil) // Create Alice Identity and Service Account Identity. - clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af@canonical.com" // alice is superuser ctx := context.Background() user := dbmodel.Identity{Name: "alice@canonical.com"} diff --git a/cmd/jaas/cmd/updatecredentials_test.go b/cmd/jaas/cmd/updatecredentials_test.go index e57a453ec..b03e7e6a8 100644 --- a/cmd/jaas/cmd/updatecredentials_test.go +++ b/cmd/jaas/cmd/updatecredentials_test.go @@ -28,7 +28,7 @@ var _ = gc.Suite(&updateCredentialsSuite{}) func (s *updateCredentialsSuite) TestUpdateCredentialsWithNewCredentials(c *gc.C) { ctx := context.Background() - clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af@canonical.com" // alice is superuser bClient := jimmtest.NewUserSessionLogin(c, "alice") @@ -69,7 +69,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithNewCredentials(c *gc.C cmdContext, err := cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(clientStore, bClient), clientID, "test-cloud", "test-credentials") c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(cmdContext), gc.Equals, `results: -- credentialtag: cloudcred-test-cloud_abda51b2-d735-4794-a8bd-49c506baa4af_test-credentials +- credentialtag: cloudcred-test-cloud_abda51b2-d735-4794-a8bd-49c506baa4af@canonical.com_test-credentials error: null models: [] `) @@ -89,7 +89,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithNewCredentials(c *gc.C func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c *gc.C) { ctx := context.Background() - clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af@canonical.com" // alice is superuser bClient := jimmtest.NewUserSessionLogin(c, "alice") @@ -139,7 +139,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c cmdContext, err := cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(clientStore, bClient), clientID, "test-cloud", "test-credentials") c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(cmdContext), gc.Equals, `results: -- credentialtag: cloudcred-test-cloud_abda51b2-d735-4794-a8bd-49c506baa4af_test-credentials +- credentialtag: cloudcred-test-cloud_abda51b2-d735-4794-a8bd-49c506baa4af@canonical.com_test-credentials error: null models: [] `) @@ -159,7 +159,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c func (s *updateCredentialsSuite) TestCloudNotInLocalStore(c *gc.C) { bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(s.ClientStore(), bClient), - "00000000-0000-0000-0000-000000000000", + "00000000-0000-0000-0000-000000000000@canonical.com", "non-existing-cloud", "foo", ) @@ -178,7 +178,7 @@ func (s *updateCredentialsSuite) TestCredentialNotInLocalStore(c *gc.C) { c.Assert(err, gc.IsNil) _, err = cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(clientStore, bClient), - "00000000-0000-0000-0000-000000000000", + "00000000-0000-0000-0000-000000000000@canonical.com", "some-cloud", "non-existing-credential-name", ) @@ -196,15 +196,15 @@ func (s *updateCredentialsSuite) TestMissingArgs(c *gc.C) { expectedError: "client ID not specified", }, { name: "missing cloud", - args: []string{"some-client-id"}, + args: []string{"some-client-id@canonical.com"}, expectedError: "cloud not specified", }, { name: "missing credential name", - args: []string{"some-client-id", "some-cloud"}, + args: []string{"some-client-id@canonical.com", "some-cloud"}, expectedError: "credential name not specified", }, { name: "too many args", - args: []string{"some-client-id", "some-cloud", "some-credential-name", "extra-arg"}, + args: []string{"some-client-id@canonical.com", "some-cloud", "some-credential-name", "extra-arg"}, expectedError: "too many args", }} diff --git a/docker-compose.yaml b/docker-compose.yaml index c7cf27977..5725cffea 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,7 +21,12 @@ services: interval: 10s timeout: 5s retries: 3 - + labels: + traefik.enable: true + traefik.http.routers.traefik.rule: Host(`127.0.0.1`) + traefik.http.routers.traefik.entrypoints: websecure + traefik.http.routers.traefik.tls: true + jimm: image: cosmtrek/air:latest profiles: ["dev"] @@ -46,7 +51,7 @@ services: JIMM_DSN: "postgresql://jimm:jimm@db/jimm" # Not needed for local test (yet). # BAKERY_AGENT_FILE: "" - JIMM_ADMINS: "jimm@candid.localhost" + JIMM_ADMINS: "jimm-test@canonical.com" # Note: You can comment out the Vault ENV vars below and instead use INSECURE_SECRET_STORAGE to place secrets in Postgres. VAULT_ADDR: "http://vault:8200" VAULT_PATH: "/jimm-kv/" diff --git a/go.mod b/go.mod index d61ff3809..1a650eb26 100644 --- a/go.mod +++ b/go.mod @@ -50,14 +50,15 @@ require ( github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/render v1.0.2 + github.com/gorilla/sessions v1.2.1 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/itchyny/gojq v0.12.12 github.com/juju/charm/v12 v12.0.0 github.com/juju/names/v5 v5.0.0 github.com/lestrrat-go/iter v1.0.2 - github.com/lestrrat-go/jwx/v2 v2.0.19 + github.com/lestrrat-go/jwx/v2 v2.0.21 github.com/oklog/ulid/v2 v2.1.0 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 golang.org/x/oauth2 v0.15.0 gopkg.in/errgo.v1 v1.0.1 gopkg.in/httprequest.v1 v1.2.1 @@ -121,7 +122,7 @@ require ( github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/tcell/v2 v2.5.1 // indirect github.com/go-goose/goose/v5 v5.0.0-20230421180421-abaee9096e3a // indirect - github.com/go-jose/go-jose/v3 v3.0.1 // indirect + github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect @@ -143,7 +144,6 @@ require ( github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/gorilla/schema v1.2.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/gorilla/sessions v1.2.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -212,7 +212,7 @@ require ( github.com/kylelemons/godebug v1.1.0 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc v1.0.4 // indirect + github.com/lestrrat-go/httprc v1.0.5 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/lestrrat/go-jspointer v0.0.0-20160229021354-f4881e611bdb // indirect github.com/lestrrat/go-jsref v0.0.0-20160601013240-e452c7b5801d // indirect @@ -251,7 +251,6 @@ require ( github.com/muhlemmer/gu v0.3.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/oracle/oci-go-sdk/v65 v65.55.0 // indirect github.com/packethost/packngo v0.28.1 // indirect @@ -296,10 +295,10 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.19.0 // indirect + golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.17.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/api v0.154.0 // indirect diff --git a/go.sum b/go.sum index d00d9fbe1..c2e7de565 100644 --- a/go.sum +++ b/go.sum @@ -255,8 +255,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-goose/goose/v5 v5.0.0-20230421180421-abaee9096e3a h1:H/l82+fC6idmYg1kfpQlCq7gYctri7AGn9RemqwN6bw= github.com/go-goose/goose/v5 v5.0.0-20230421180421-abaee9096e3a/go.mod h1:BxICmnmP7QlxZhKP2BHkpWQS0tbb3LrsrLtd9TQyyms= -github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= -github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= +github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -294,8 +294,6 @@ github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZg github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= -github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -745,12 +743,12 @@ github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= -github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk= +github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY= -github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU= +github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55FHrR0= +github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat/go-jspointer v0.0.0-20160229021354-f4881e611bdb h1:ZWuRImtpQp2QxwzMFDYqSgym24d7N0HE38JRVoJ/Piw= @@ -883,8 +881,6 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= -github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk= -github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= @@ -1010,8 +1006,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1021,8 +1018,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= @@ -1122,8 +1120,9 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1162,6 +1161,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1210,6 +1210,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1238,6 +1240,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1305,18 +1308,23 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1328,6 +1336,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1395,6 +1405,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 634ecd882..dcf3e9edd 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -245,7 +245,7 @@ func TestVerifyClientCredentials(t *testing.T) { const ( // these are valid client credentials hardcoded into the jimm realm - validClientID = "test-client-id" + validClientID = "test-client-id@canonical.com" validClientSecret = "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf" ) diff --git a/internal/jimm/service_account_test.go b/internal/jimm/service_account_test.go index 2da83030f..302ba3222 100644 --- a/internal/jimm/service_account_test.go +++ b/internal/jimm/service_account_test.go @@ -35,7 +35,7 @@ func TestAddServiceAccount(t *testing.T) { }, client, ) - clientID := "39caae91-b914-41ae-83f8-c7b86ca5ad5a" + clientID := "39caae91-b914-41ae-83f8-c7b86ca5ad5a@canonical.com" err = j.AddServiceAccount(ctx, user, clientID) c.Assert(err, qt.IsNil) err = j.AddServiceAccount(ctx, user, clientID) @@ -73,7 +73,7 @@ func TestGrantServiceAccountAccess(t *testing.T) { "user-bob", "group-1#member", }, - clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", username: "alice", }, { about: "Group that doesn't exist", @@ -86,7 +86,7 @@ func TestGrantServiceAccountAccess(t *testing.T) { // This group doesn't exist. "group-bar", }, - clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", username: "alice", expectedError: "group bar not found", }, { @@ -99,7 +99,7 @@ func TestGrantServiceAccountAccess(t *testing.T) { "user-bob", "controller-jimm", }, - clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", username: "alice", expectedError: "invalid entity - not user or group", }} diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index 239c32f75..4f6a827ee 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -305,7 +305,7 @@ func (s *adminSuite) TestLoginWithClientCredentials(c *gc.C) { const ( // these are valid client credentials hardcoded into the jimm realm - validClientID = "test-client-id" + validClientID = "test-client-id@canonical.com" validClientSecret = "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf" ) @@ -316,7 +316,7 @@ func (s *adminSuite) TestLoginWithClientCredentials(c *gc.C) { }, &loginResult) c.Assert(err, gc.IsNil) c.Assert(loginResult.ControllerTag, gc.Equals, names.NewControllerTag(s.Params.ControllerUUID).String()) - c.Assert(loginResult.UserInfo.Identity, gc.Equals, names.NewUserTag("test-client-id").String()) + c.Assert(loginResult.UserInfo.Identity, gc.Equals, names.NewUserTag("test-client-id@canonical.com").String()) err = conn.APICall("Admin", 4, "", "LoginWithClientCredentials", params.LoginWithClientCredentialsRequest{ ClientID: "invalid-client-id", diff --git a/internal/jujuapi/service_account_test.go b/internal/jujuapi/service_account_test.go index 07e044875..531ab2935 100644 --- a/internal/jujuapi/service_account_test.go +++ b/internal/jujuapi/service_account_test.go @@ -38,7 +38,7 @@ func TestAddServiceAccount(t *testing.T) { return nil }, args: params.AddServiceAccountRequest{ - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", }, }, { about: "Invalid Client ID", @@ -80,17 +80,17 @@ func TestGetServiceAccount(t *testing.T) { expectedError string }{{ about: "Valid request", - clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", username: "alice", addTuples: []openfga.Tuple{{ Object: ofganames.ConvertTag(names.NewUserTag("alice")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com")), }}, }, { about: "Missing service account administrator permission", username: "alice", - clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", expectedError: "unauthorized", }, { about: "Invalid Client ID", @@ -164,7 +164,7 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { }, }}, args: params.UpdateServiceAccountCredentialsRequest{ - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ Credentials: []jujuparams.TaggedCredential{ { @@ -181,7 +181,7 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { addTuples: []openfga.Tuple{{ Object: ofganames.ConvertTag(names.NewUserTag("alice")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com")), }}, }, { about: "Invalid Credential Tag", @@ -199,7 +199,7 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { }, }}, args: params.UpdateServiceAccountCredentialsRequest{ - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ Credentials: []jujuparams.TaggedCredential{ { @@ -212,7 +212,7 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { addTuples: []openfga.Tuple{{ Object: ofganames.ConvertTag(names.NewUserTag("alice")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com")), }}, }, { about: "Invalid Service account ID", @@ -237,7 +237,7 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { return nil, nil }, args: params.UpdateServiceAccountCredentialsRequest{ - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ Credentials: []jujuparams.TaggedCredential{ { @@ -307,7 +307,7 @@ func TestListServiceAccountCredentials(t *testing.T) { expectedResult: jujuparams.CredentialContentResults{ Results: []jujuparams.CredentialContentResult{}}, args: params.ListServiceAccountCredentialsRequest{ - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", }, getCloudCredential: func(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) { cred := &dbmodel.CloudCredential{} @@ -320,7 +320,7 @@ func TestListServiceAccountCredentials(t *testing.T) { addTuples: []openfga.Tuple{{ Object: ofganames.ConvertTag(names.NewUserTag("alice")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com")), }}, }, { about: "Invalid Service account ID", @@ -345,7 +345,7 @@ func TestListServiceAccountCredentials(t *testing.T) { return nil }, args: params.ListServiceAccountCredentialsRequest{ - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", }, getCloudCredential: func(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) { cred := &dbmodel.CloudCredential{} @@ -417,13 +417,13 @@ func TestGrantServiceAccountAccess(t *testing.T) { "user-alice", "user-bob", }, - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", }, username: "alice", addTuples: []openfga.Tuple{{ Object: ofganames.ConvertTag(names.NewUserTag("alice")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be")), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com")), }}, }, { about: "Invalid Service account ID", @@ -449,7 +449,7 @@ func TestGrantServiceAccountAccess(t *testing.T) { "user-alice", "user-bob", }, - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", }, username: "alice", expectedError: "unauthorized", @@ -501,7 +501,7 @@ func (s *serviceAccountSuite) TestUpdateServiceAccountCredentialsIntegration(c * conn := s.open(c, nil, "bob") defer conn.Close() - serviceAccount := jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be") + serviceAccount := jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com") tuple := openfga.Tuple{ Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), @@ -517,15 +517,15 @@ func (s *serviceAccountSuite) TestUpdateServiceAccountCredentialsIntegration(c * var credResults jujuparams.UpdateCredentialResults err := conn.APICall("JIMM", 4, "", "UpdateServiceAccountCredentials", params.UpdateServiceAccountCredentialsRequest{ - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ Credentials: []jujuparams.TaggedCredential{ { - Tag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be/cred-name", + Tag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com/cred-name", Credential: jujuparams.CloudCredential{Attributes: map[string]string{"foo": "bar"}}, }, { - Tag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be/cred-name2", + Tag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com/cred-name2", Credential: jujuparams.CloudCredential{Attributes: map[string]string{"wolf": "low"}}, }, }}, @@ -534,12 +534,12 @@ func (s *serviceAccountSuite) TestUpdateServiceAccountCredentialsIntegration(c * expectedResult := jujuparams.UpdateCredentialResults{ Results: []jujuparams.UpdateCredentialResult{ { - CredentialTag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be/cred-name", + CredentialTag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com/cred-name", Error: nil, Models: nil, }, { - CredentialTag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be/cred-name2", + CredentialTag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com/cred-name2", Error: nil, Models: nil, }, diff --git a/internal/rpc/proxy.go b/internal/rpc/proxy.go index 12e2ab0f3..4357ccc24 100644 --- a/internal/rpc/proxy.go +++ b/internal/rpc/proxy.go @@ -609,9 +609,12 @@ func (p *clientProxy) handleAdminFacade(ctx context.Context, msg *message) (clie } // Verify the session token - // TODO(CSS-7081): Ensure for tests that the secret key can be configured. - // Or configure cmd tests to use the configured secret. - token, err := p.jimm.OAuthAuthenticationService().VerifySessionToken(request.SessionToken, "test-secret") + secretKey, err := p.jimm.GetCredentialStore().GetOAuthSecret(ctx) + if err != nil { + return errorFnc(err) + } + + token, err := p.jimm.OAuthAuthenticationService().VerifySessionToken(request.SessionToken, string(secretKey)) if err != nil { return errorFnc(err) } diff --git a/local/jimm/setup-controller.sh b/local/jimm/setup-controller.sh index a18833ec9..8bb982336 100755 --- a/local/jimm/setup-controller.sh +++ b/local/jimm/setup-controller.sh @@ -23,4 +23,4 @@ CLOUDINIT_TEMPLATE=$'cloudinit-userdata: | printf "$CLOUDINIT_TEMPLATE" "$(lxc network get lxdbr0 ipv4.address | cut -f1 -d/)" "$(cat local/traefik/certs/ca.crt | sed -e 's/^/ /')" > "${CLOUDINIT_FILE}" echo "Bootstrapping controller" -juju bootstrap localhost "${CONTROLLER_NAME}" --config allow-model-access=true --config "${CLOUDINIT_FILE}" --config login-token-refresh-url=https://jimm.localhost/.well-known/jwks.json +juju bootstrap lxd "${CONTROLLER_NAME}" --config "${CLOUDINIT_FILE}" --config login-token-refresh-url=https://jimm.localhost/.well-known/jwks.json --debug diff --git a/local/keycloak/jimm-realm.json b/local/keycloak/jimm-realm.json index cf0a1488d..67c223e28 100644 --- a/local/keycloak/jimm-realm.json +++ b/local/keycloak/jimm-realm.json @@ -693,7 +693,7 @@ ] }, { - "clientId": "test-client-id", + "clientId": "test-client-id@canonical.com", "name": "", "description": "", "rootUrl": "", diff --git a/local/traefik/traefik.yaml b/local/traefik/traefik.yaml index 6285cf2c2..00585c0e5 100644 --- a/local/traefik/traefik.yaml +++ b/local/traefik/traefik.yaml @@ -41,17 +41,18 @@ entryPoints: websecure: address: :443 - ## DYNAMIC CONFIG tls: certificates: - certFile: /certs/server.crt keyFile: /certs/server.key - -# when troubleshooting certs, enable this so traefik doesn't use + default: + keyFile: /certs/server.key + certFile: /certs/server.crt +# when troubleshooting certs, enable this so traefik doesn't use # its own self-signed. By default if it can't find a matching # cert, it'll just create its own which will cause cert warnings # in browser and can be confusing to troubleshoot - # options: - # default: - # sniStrict: true +# options: +# default: +# sniStrict: true diff --git a/pkg/names/service_account.go b/pkg/names/service_account.go index 585d62864..ae6c2c424 100644 --- a/pkg/names/service_account.go +++ b/pkg/names/service_account.go @@ -6,7 +6,8 @@ package names import ( "fmt" - "regexp" + + "github.com/juju/names/v5" ) const ( @@ -15,11 +16,6 @@ const ( ServiceAccountTagKind = "serviceaccount" ) -var ( - validClientIdSnippet = `^[0-9a-zA-Z-]+$` - validClientId = regexp.MustCompile(validClientIdSnippet) -) - // ServiceAccount represents a service account where id is the client ID. // Implements juju names.Tag. type ServiceAccountTag struct { @@ -38,13 +34,11 @@ func (t ServiceAccountTag) String() string { return ServiceAccountTagKind + "-" // NewServiceAccountTag creates a valid ServiceAccountTag if it is possible to parse // the provided tag. func NewServiceAccountTag(clientId string) ServiceAccountTag { - id := validClientId.FindString(clientId) - if !IsValidServiceAccountId(clientId) { panic(fmt.Sprintf("invalid client tag %q", clientId)) } - return ServiceAccountTag{id: id} + return ServiceAccountTag{id: clientId} } // ParseServiceAccountTag parses a service account tag string. @@ -62,5 +56,9 @@ func ParseServiceAccountTag(tag string) (ServiceAccountTag, error) { // IsValidServiceAccountId verifies the client id for a service account is valid according to a regex internally. func IsValidServiceAccountId(id string) bool { - return validClientId.MatchString(id) + if !names.IsValidUser(id) { + return false + } + t := names.NewUserTag(id) + return t.Domain() != "" } diff --git a/pkg/names/service_account_test.go b/pkg/names/service_account_test.go index 2f2b29434..156fb8869 100644 --- a/pkg/names/service_account_test.go +++ b/pkg/names/service_account_test.go @@ -14,16 +14,20 @@ func TestParseServiceAccountID(t *testing.T) { err string }{{ about: "Valid svc account tag", - tag: "serviceaccount-1e654457-a195-4a41-8360-929c7f455d43", - expectedID: "1e654457-a195-4a41-8360-929c7f455d43", + tag: "serviceaccount-1e654457-a195-4a41-8360-929c7f455d43@canonical.com", + expectedID: "1e654457-a195-4a41-8360-929c7f455d43@canonical.com", err: "", + }, { + about: "Invalid svc account tag (no domain)", + tag: "serviceaccount-1e654457-a195-4a41-8360-929c7f455d43", + err: "is not a valid serviceaccount tag", }, { about: "Invalid svc account tag (serviceaccounts)", - tag: "serviceaccounts-1e654457-a195-4a41-8360-929c7f455d43", + tag: "serviceaccounts-1e654457-a195-4a41-8360-929c7f455d43@canonical.com", err: "is not a valid tag", }, { about: "Invalid svc account tag (no prefix)", - tag: "1e654457-a195-4a41-8360-929c7f455d43", + tag: "1e654457-a195-4a41-8360-929c7f455d43@canonical.com", err: "is not a valid tag", }, { about: "Invalid svc account tag (missing ID)", @@ -47,10 +51,12 @@ func TestParseServiceAccountID(t *testing.T) { } func TestIsValidServiceAccountId(t *testing.T) { - assert.True(t, IsValidServiceAccountId("1e654457-a195-4a41-8360-929c7f455d43")) - assert.True(t, IsValidServiceAccountId("12345")) - assert.True(t, IsValidServiceAccountId("abc123")) - assert.True(t, IsValidServiceAccountId("ABC123")) + assert.True(t, IsValidServiceAccountId("1e654457-a195-4a41-8360-929c7f455d43@canonical.com")) + assert.True(t, IsValidServiceAccountId("12345@canonical.com")) + assert.True(t, IsValidServiceAccountId("abc123@canonical.com")) + assert.True(t, IsValidServiceAccountId("ABC123@canonical.com")) + assert.True(t, IsValidServiceAccountId("ABC123@canonical.com")) + assert.False(t, IsValidServiceAccountId("ABC123")) assert.False(t, IsValidServiceAccountId("abc 123")) assert.False(t, IsValidServiceAccountId("")) assert.False(t, IsValidServiceAccountId(" ")) From 9db4362502da66ed60ba58710dc0b8acce7c9a19 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:37:43 +0200 Subject: [PATCH 102/126] Removed candid config from k8s charm (#1188) * Removed candid config from k8s charm * Add an extra test * Fix tests * Further test fixes * Fix docker compose --- charms/jimm-k8s/config.yaml | 18 --- charms/jimm-k8s/src/charm.py | 34 +---- charms/jimm-k8s/tests/unit/test_charm.py | 94 +++++++------- cmd/jimmctl/cmd/listcontrollers_test.go | 2 +- docker-compose.yaml | 4 +- internal/cmdtest/jimmsuite.go | 36 ++---- internal/discharger/discharger.go | 6 +- internal/jimmjwx/utils_test.go | 40 +++--- internal/jimmtest/jimm.go | 28 +++++ internal/jimmtest/suite.go | 2 + service_test.go | 152 +++++------------------ 11 files changed, 136 insertions(+), 280 deletions(-) create mode 100644 internal/jimmtest/jimm.go diff --git a/charms/jimm-k8s/config.yaml b/charms/jimm-k8s/config.yaml index 72457fc34..cb3f8223f 100644 --- a/charms/jimm-k8s/config.yaml +++ b/charms/jimm-k8s/config.yaml @@ -10,24 +10,6 @@ options: Logs are purged at 9AM UTC. Defaults to 0, which means by default logs are never purged. default: "0" - candid-agent-private-key: - type: string - description: | - Private key of the agent user for accessing the candid API. - candid-agent-public-key: - type: string - description: | - Public key of the agent user for accessing the candid API. - candid-agent-username: - type: string - description: | - Name of the agent user for accessing the candid API. - candid-url: - type: string - description: URL of candid server. - candid-public-key: - type: string - description: Candid public key. controller-admins: type: string description: | diff --git a/charms/jimm-k8s/src/charm.py b/charms/jimm-k8s/src/charm.py index 49c9fe09d..6d071ce2e 100755 --- a/charms/jimm-k8s/src/charm.py +++ b/charms/jimm-k8s/src/charm.py @@ -59,13 +59,14 @@ REQUIRED_SETTINGS = { "JIMM_UUID": "missing uuid configuration", "JIMM_DSN": "missing postgresql relation", - "CANDID_URL": "missing candid-url configuration", "OPENFGA_STORE": "missing openfga relation", "OPENFGA_AUTH_MODEL": "run create-authorization-model action", "OPENFGA_HOST": "missing openfga relation", "OPENFGA_SCHEME": "missing openfga relation", "OPENFGA_TOKEN": "missing openfga relation", "OPENFGA_PORT": "missing openfga relation", + "BAKERY_PRIVATE_KEY": "missing private key configuration", + "BAKERY_PUBLIC_KEY": "missing public key configuration", } JIMM_SERVICE_NAME = "jimm" @@ -193,9 +194,7 @@ def __init__(self, *args): self._on_create_authorization_model_action, ) - self._local_agent_filename = "agent.json" self._local_vault_secret_filename = "vault_secret.js" - self._agent_filename = "/root/config/agent.json" self._vault_secret_filename = "/root/config/vault_secret.json" self._vault_path = "charm-jimm-k8s-creds" @@ -219,23 +218,6 @@ def _on_leader_elected(self, event): self._update_workload(event) - def _ensure_bakery_agent_file(self, event): - # we create the file containing agent keys if needed. - if not self._path_exists_in_workload(self._agent_filename): - url = self.config.get("candid-url", "") - username = self.config.get("candid-agent-username", "") - private_key = self.config.get("candid-agent-private-key", "") - public_key = self.config.get("candid-agent-public-key", "") - if not url or not username or not private_key or not public_key: - return "" - data = { - "key": {"public": public_key, "private": private_key}, - "agents": [{"url": url, "username": username}], - } - agent_data = json.dumps(data) - - self._push_to_workload(self._agent_filename, agent_data, event) - @requires_state def _update_workload(self, event): """' Update workload with all available configuration @@ -248,7 +230,6 @@ def _update_workload(self, event): return self.oauth.update_client_config(client_config=self._oauth_client_config) - self._ensure_bakery_agent_file(event) self._ensure_vault_file(event) if self.model.get_relation("vault") and not container.exists(self._vault_secret_filename): logger.warning("Vault relation present but vault setup is not ready yet") @@ -268,8 +249,6 @@ def _update_workload(self, event): oauth_provider_info = self.oauth.get_provider_info() config_values = { - "CANDID_PUBLIC_KEY": self.config.get("candid-public-key", ""), - "CANDID_URL": self.config.get("candid-url", ""), "JIMM_AUDIT_LOG_RETENTION_PERIOD_IN_DAYS": self.config.get("audit-log-retention-period-in-days", ""), "JIMM_ADMINS": self.config.get("controller-admins", ""), "JIMM_DNS_NAME": dns_name, @@ -283,8 +262,8 @@ def _update_workload(self, event): "OPENFGA_SCHEME": self._state.openfga_scheme, "OPENFGA_TOKEN": self._state.openfga_token, "OPENFGA_PORT": self._state.openfga_port, - "PRIVATE_KEY": self.config.get("private-key", ""), - "PUBLIC_KEY": self.config.get("public-key", ""), + "BAKERY_PRIVATE_KEY": self.config.get("private-key", ""), + "BAKERY_PUBLIC_KEY": self.config.get("public-key", ""), "JIMM_JWT_EXPIRY": self.config.get("jwt-expiry"), "JIMM_MACAROON_EXPIRY_DURATION": self.config.get("macaroon-expiry-duration", "24h"), "JIMM_ACCESS_TOKEN_EXPIRY_DURATION": self.config.get("session-expiry-duration"), @@ -299,9 +278,6 @@ def _update_workload(self, event): if self._state.dsn: config_values["JIMM_DSN"] = self._state.dsn - if container.exists(self._agent_filename): - config_values["BAKERY_AGENT_FILE"] = self._agent_filename - if container.exists(self._vault_secret_filename): config_values["VAULT_ADDR"] = self._state.vault_address config_values["VAULT_PATH"] = self._vault_path @@ -363,7 +339,6 @@ def _update_workload(self, event): dashboard_relation.data[self.app].update( { "controller_url": "wss://{}".format(dns_name), - "identity_provider_url": self.config.get("candid-url"), "is_juju": str(False), } ) @@ -407,7 +382,6 @@ def _on_dashboard_relation_joined(self, event: RelationJoinedEvent): event.relation.data[self.app].update( { "controller_url": "wss://{}".format(dns_name), - "identity_provider_url": self.config["candid-url"], "is_juju": str(False), } ) diff --git a/charms/jimm-k8s/tests/unit/test_charm.py b/charms/jimm-k8s/tests/unit/test_charm.py index f71d397a9..32c2717e0 100644 --- a/charms/jimm-k8s/tests/unit/test_charm.py +++ b/charms/jimm-k8s/tests/unit/test_charm.py @@ -10,7 +10,7 @@ import unittest from unittest.mock import patch -from ops.model import BlockedStatus +from ops.model import ActiveStatus, BlockedStatus from ops.testing import Harness from src.charm import JimmOperatorCharm @@ -27,9 +27,16 @@ "userinfo_endpoint": "https://example.oidc.com/userinfo", } +OPENFGA_PROVIDER_INFO = { + "address": "openfga.localhost", + "port": "8080", + "scheme": "http", + "store_id": "fake-store-id", + "token": "fake-token", +} + MINIMAL_CONFIG = { "uuid": "1234567890", - "candid-url": "test-candid-url", "public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", "vault-access-address": "10.0.1.123", @@ -37,7 +44,6 @@ } EXPECTED_ENV = { - "CANDID_URL": "test-candid-url", "JIMM_DASHBOARD_LOCATION": "https://jaas.ai/models", "JIMM_DNS_NAME": "juju-jimm-k8s-0.juju-jimm-k8s-endpoints.None.svc.cluster.local", "JIMM_ENABLE_JWKS_ROTATOR": "1", @@ -45,8 +51,8 @@ "JIMM_LOG_LEVEL": "info", "JIMM_UUID": "1234567890", "JIMM_WATCH_CONTROLLERS": "1", - "PRIVATE_KEY": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", - "PUBLIC_KEY": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", + "BAKERY_PRIVATE_KEY": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", + "BAKERY_PUBLIC_KEY": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", "JIMM_AUDIT_LOG_RETENTION_PERIOD_IN_DAYS": "0", "JIMM_MACAROON_EXPIRY_DURATION": "24h", "JIMM_JWT_EXPIRY": "5m", @@ -123,6 +129,17 @@ def setUp(self): }, ) + def add_openfga_relation(self): + self.openfga_rel_id = self.harness.add_relation("openfga", "openfga") + self.harness.add_relation_unit(self.openfga_rel_id, "openfga/0") + self.harness.update_relation_data( + self.openfga_rel_id, + "openfga", + { + **OPENFGA_PROVIDER_INFO, + }, + ) + # import ipdb; ipdb.set_trace() def test_on_pebble_ready(self): self.harness.update_config(MINIMAL_CONFIG) @@ -209,43 +226,6 @@ def test_app_enters_block_state_if_oauth_relation_not_ready(self): self.assertEqual(self.harness.charm.unit.status.name, BlockedStatus.name) self.assertEqual(self.harness.charm.unit.status.message, "Waiting for OAuth relation") - def test_bakery_configuration(self): - container = self.harness.model.unit.get_container("jimm") - self.harness.charm.on.jimm_pebble_ready.emit(container) - - self.harness.update_config( - { - "uuid": "1234567890", - "candid-url": "test-candid-url", - "candid-agent-username": "test-username", - "candid-agent-public-key": "test-public-key", - "candid-agent-private-key": "test-private-key", - "public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", - "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", - "final-redirect-url": "some-url", - } - ) - - # Emit the pebble-ready event for jimm - self.harness.charm.on.jimm_pebble_ready.emit(container) - expected_env = EXPECTED_ENV.copy() - expected_env.update({"BAKERY_AGENT_FILE": "/root/config/agent.json"}) - # Check the that the plan was updated - plan = self.harness.get_container_pebble_plan("jimm") - self.assertEqual(plan.to_dict(), get_expected_plan(expected_env)) - agent_data = container.pull("/root/config/agent.json") - agent_json = json.loads(agent_data.read()) - self.assertEqual( - agent_json, - { - "key": { - "public": "test-public-key", - "private": "test-private-key", - }, - "agents": [{"url": "test-candid-url", "username": "test-username"}], - }, - ) - def test_audit_log_retention_config(self): container = self.harness.model.unit.get_container("jimm") self.harness.charm.on.jimm_pebble_ready.emit(container) @@ -271,10 +251,6 @@ def test_dashboard_relation_joined(self): harness.set_leader(True) harness.update_config( { - "candid-agent-username": "username@candid", - "candid-agent-private-key": "agent-private-key", - "candid-agent-public-key": "agent-public-key", - "candid-url": "https://candid.example.com", "controller-admins": "user1 user2 group1", "uuid": "caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa", } @@ -289,7 +265,6 @@ def test_dashboard_relation_joined(self): data["controller_url"], "wss://juju-jimm-k8s-0.juju-jimm-k8s-endpoints.None.svc.cluster.local", ) - self.assertEqual(data["identity_provider_url"], "https://candid.example.com") self.assertEqual(data["is_juju"], "False") @patch("socket.gethostname") @@ -312,10 +287,6 @@ def test_vault_relation_joined(self, hvac_client_sys, gethostname): harness.update_config( { - "candid-agent-username": "username@candid", - "candid-agent-private-key": "agent-private-key", - "candid-agent-public-key": "agent-public-key", - "candid-url": "https://candid.example.com", "controller-admins": "user1 user2 group1", "uuid": "caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa", "vault-access-address": "10.0.1.123", @@ -384,3 +355,24 @@ def test_app_raises_error_without_vault_config(self): with self.assertRaises(ValueError) as e: self.harness.add_relation_unit(id, "vault/0") self.assertEqual(e, "Missing config vault-access-address for vault relation") + + def test_app_blocked_without_private_key(self): + self.harness.enable_hooks() + # Fake the Postgres relation. + self.harness.charm._state.dsn = "postgres-dsn" + # Setup the OpenFGA relation. + self.add_openfga_relation() + self.harness.charm._state.openfga_auth_model_id = 1 + # Set the config with the private-key value missing. + min_config_no_private_key = MINIMAL_CONFIG.copy() + del min_config_no_private_key["private-key"] + self.harness.update_config(min_config_no_private_key) + self.assertEqual(self.harness.charm.unit.status.name, BlockedStatus.name) + self.assertEqual( + self.harness.charm.unit.status.message, + "BAKERY_PRIVATE_KEY configuration value not set: missing private key configuration", + ) + # Now check that we can get the app into an active state. + self.harness.update_config(MINIMAL_CONFIG) + self.assertEqual(self.harness.charm.unit.status.name, ActiveStatus.name) + self.assertEqual(self.harness.charm.unit.status.message, "running") diff --git a/cmd/jimmctl/cmd/listcontrollers_test.go b/cmd/jimmctl/cmd/listcontrollers_test.go index e23f3c331..62917adf7 100644 --- a/cmd/jimmctl/cmd/listcontrollers_test.go +++ b/cmd/jimmctl/cmd/listcontrollers_test.go @@ -51,7 +51,7 @@ var ( ` expectedOutput = `- name: jaas - uuid: 914487b5-60e7-42bb-bd63-1adc3fd3a388 + uuid: 6acf4fd8-32d6-49ea-b4eb-dcb9d1590c11 publicaddress: "" apiaddresses: \[\] cacertificate: "" diff --git a/docker-compose.yaml b/docker-compose.yaml index 5725cffea..f62dfe6ff 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -68,8 +68,8 @@ services: JIMM_ENABLE_JWKS_ROTATOR: "1" JIMM_AUDIT_LOG_RETENTION_PERIOD_IN_DAYS: "1" TEST_LOGGING_CONFIG: "" - PUBLIC_KEY: "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=" - PRIVATE_KEY: "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=" + BAKERY_PUBLIC_KEY: "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=" + BAKERY_PRIVATE_KEY: "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=" OPENFGA_SCHEME: "http" OPENFGA_HOST: "openfga" OPENFGA_PORT: 8080 diff --git a/internal/cmdtest/jimmsuite.go b/internal/cmdtest/jimmsuite.go index c35b1eb5b..5ccdb7f4c 100644 --- a/internal/cmdtest/jimmsuite.go +++ b/internal/cmdtest/jimmsuite.go @@ -18,7 +18,6 @@ import ( "time" cofga "github.com/canonical/ofga" - "github.com/coreos/go-oidc/v3/oidc" "github.com/juju/juju/api" "github.com/juju/juju/core/network" corejujutesting "github.com/juju/juju/juju/testing" @@ -67,30 +66,19 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { s.COFGAClient = cofgaClient s.COFGAParams = cofgaParams - s.Params = service.Params{ - PublicDNSName: u.Host, - ControllerUUID: "914487b5-60e7-42bb-bd63-1adc3fd3a388", - ControllerAdmins: []string{"admin"}, - DSN: jimmtest.CreateEmptyDatabase(&jimmtest.GocheckTester{c}), - OpenFGAParams: service.OpenFGAParams{ - Scheme: cofgaParams.Scheme, - Host: cofgaParams.Host, - Port: cofgaParams.Port, - Store: cofgaParams.StoreID, - Token: cofgaParams.Token, - AuthModel: cofgaParams.AuthModelID, - }, - JWTExpiryDuration: time.Minute, - InsecureSecretStorage: true, - OAuthAuthenticatorParams: service.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), - SessionCookieMaxAge: 60, - }, - DashboardFinalRedirectURL: "", + s.Params = jimmtest.NewTestJimmParams(&jimmtest.GocheckTester{C: c}) + s.Params.PublicDNSName = u.Host + s.Params.ControllerAdmins = []string{"admin"} + s.Params.OpenFGAParams = service.OpenFGAParams{ + Scheme: cofgaParams.Scheme, + Host: cofgaParams.Host, + Port: cofgaParams.Port, + Store: cofgaParams.StoreID, + Token: cofgaParams.Token, + AuthModel: cofgaParams.AuthModelID, } + s.Params.JWTExpiryDuration = time.Minute + s.Params.InsecureSecretStorage = true srv, err := service.NewService(ctx, s.Params) c.Assert(err, gc.Equals, nil) diff --git a/internal/discharger/discharger.go b/internal/discharger/discharger.go index cbb985359..a30b3a813 100644 --- a/internal/discharger/discharger.go +++ b/internal/discharger/discharger.go @@ -37,11 +37,7 @@ type MacaroonDischargerConfig struct { func NewMacaroonDischarger(cfg MacaroonDischargerConfig, db *db.Database, ofgaClient *openfga.OFGAClient) (*MacaroonDischarger, error) { var kp bakery.KeyPair if cfg.PublicKey == "" || cfg.PrivateKey == "" { - generatedKP, err := bakery.GenerateKey() - if err != nil { - return nil, errors.E(err, "failed to generate a bakery keypair") - } - kp = *generatedKP + return nil, errors.E("missing bakery private/public key") } else { if err := kp.Private.UnmarshalText([]byte(cfg.PrivateKey)); err != nil { return nil, errors.E(err, "cannot unmarshal private key") diff --git a/internal/jimmjwx/utils_test.go b/internal/jimmjwx/utils_test.go index 3e31b7537..9eb63bb82 100644 --- a/internal/jimmjwx/utils_test.go +++ b/internal/jimmjwx/utils_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" "github.com/google/uuid" "github.com/lestrrat-go/jwx/v2/jwk" @@ -93,30 +92,21 @@ func setupService(ctx context.Context, c *qt.C) (*jimm.Service, *httptest.Server _, _, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) - svc, err := jimm.NewService(context.Background(), jimm.Params{ - ControllerUUID: "6acf4fd8-32d6-49ea-b4eb-dcb9d1590c11", - DSN: jimmtest.CreateEmptyDatabase(c), - VaultAddress: "http://localhost:8200", - VaultAuthPath: "/auth/approle/login", - VaultPath: "/jimm-kv/", - VaultSecretFile: "../../local/vault/approle.json", - OpenFGAParams: jimm.OpenFGAParams{ - Scheme: cofgaParams.Scheme, - Host: cofgaParams.Host, - Port: cofgaParams.Port, - Store: cofgaParams.StoreID, - Token: cofgaParams.Token, - AuthModel: cofgaParams.AuthModelID, - }, - OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), - SessionCookieMaxAge: 60, - }, - DashboardFinalRedirectURL: "", - }) + p := jimmtest.NewTestJimmParams(c) + p.VaultAddress = "http://localhost:8200" + p.VaultAuthPath = "/auth/approle/login" + p.VaultPath = "/jimm-kv/" + p.VaultSecretFile = "../../local/vault/approle.json" + p.OpenFGAParams = jimm.OpenFGAParams{ + Scheme: cofgaParams.Scheme, + Host: cofgaParams.Host, + Port: cofgaParams.Port, + Store: cofgaParams.StoreID, + Token: cofgaParams.Token, + AuthModel: cofgaParams.AuthModelID, + } + svc, err := jimm.NewService(context.Background(), p) + c.Assert(err, qt.IsNil) srv := httptest.NewTLSServer(svc) diff --git a/internal/jimmtest/jimm.go b/internal/jimmtest/jimm.go new file mode 100644 index 000000000..86e03fdad --- /dev/null +++ b/internal/jimmtest/jimm.go @@ -0,0 +1,28 @@ +package jimmtest + +import ( + "time" + + "github.com/canonical/jimm" + "github.com/coreos/go-oidc/v3/oidc" +) + +// NewTestJimmParams returns a set of JIMM params with sensible defaults +// for tests. A test can override any parameter that it needs. +// Note that NewTestJimmParams will create an empty test database. +func NewTestJimmParams(t Tester) jimm.Params { + return jimm.Params{ + DSN: CreateEmptyDatabase(t), + ControllerUUID: "6acf4fd8-32d6-49ea-b4eb-dcb9d1590c11", + PrivateKey: "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", + PublicKey: "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", + OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ + IssuerURL: "http://localhost:8082/realms/jimm", + ClientID: "jimm-device", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + SessionTokenExpiry: time.Duration(time.Hour), + SessionCookieMaxAge: 60, + }, + DashboardFinalRedirectURL: "dashboard-url", + } +} diff --git a/internal/jimmtest/suite.go b/internal/jimmtest/suite.go index 49d872095..691ad6016 100644 --- a/internal/jimmtest/suite.go +++ b/internal/jimmtest/suite.go @@ -152,6 +152,8 @@ func (s *JIMMSuite) setupMacaroonDischarger(c *gc.C) *discharger.MacaroonDischar cfg := discharger.MacaroonDischargerConfig{ MacaroonExpiryDuration: 1 * time.Hour, ControllerUUID: s.JIMM.UUID, + PrivateKey: "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", + PublicKey: "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", } macaroonDischarger, err := discharger.NewMacaroonDischarger(cfg, &s.JIMM.Database, s.JIMM.OpenFGAClient) c.Assert(err, gc.IsNil) diff --git a/service_test.go b/service_test.go index b243fa0c3..17fd9d846 100644 --- a/service_test.go +++ b/service_test.go @@ -10,10 +10,8 @@ import ( "net/http/httptest" "os" "testing" - "time" cofga "github.com/canonical/ofga" - "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" @@ -42,19 +40,9 @@ func TestDefaultService(t *testing.T) { _, _, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) - svc, err := jimm.NewService(context.Background(), jimm.Params{ - DSN: jimmtest.CreateEmptyDatabase(c), - OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), - InsecureSecretStorage: true, - OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), - SessionCookieMaxAge: 60, - }, - DashboardFinalRedirectURL: "", - }) + p := jimmtest.NewTestJimmParams(c) + p.OpenFGAParams = cofgaParamsToJIMMOpenFGAParams(*cofgaParams) + svc, err := jimm.NewService(context.Background(), p) c.Assert(err, qt.IsNil) rr := httptest.NewRecorder() req, err := http.NewRequest("GET", "/debug/info", nil) @@ -69,18 +57,9 @@ func TestServiceStartsWithoutSecretStore(t *testing.T) { _, _, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) - _, err = jimm.NewService(context.Background(), jimm.Params{ - DSN: jimmtest.CreateEmptyDatabase(c), - OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), - OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), - SessionCookieMaxAge: 60, - }, - DashboardFinalRedirectURL: "", - }) + p := jimmtest.NewTestJimmParams(c) + p.OpenFGAParams = cofgaParamsToJIMMOpenFGAParams(*cofgaParams) + _, err = jimm.NewService(context.Background(), p) c.Assert(err, qt.IsNil) } @@ -91,22 +70,10 @@ func TestAuthenticator(t *testing.T) { _, _, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) - p := jimm.Params{ - ControllerUUID: "6acf4fd8-32d6-49ea-b4eb-dcb9d1590c11", - DSN: jimmtest.CreateEmptyDatabase(c), - ControllerAdmins: []string{"admin"}, - OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), - InsecureSecretStorage: true, - OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), - SessionCookieMaxAge: 60, - }, - DashboardFinalRedirectURL: "", - } - svc, err := jimm.NewService(ctx, p) + p := jimmtest.NewTestJimmParams(c) + p.InsecureSecretStorage = true + p.OpenFGAParams = cofgaParamsToJIMMOpenFGAParams(*cofgaParams) + svc, err := jimm.NewService(context.Background(), p) c.Assert(err, qt.IsNil) err = svc.JIMM().GetCredentialStore().PutOAuthSecret(ctx, []byte(jimmtest.JWTTestSecret)) @@ -163,25 +130,13 @@ func TestVault(t *testing.T) { ofgaClient, _, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) - p := jimm.Params{ - ControllerUUID: "6acf4fd8-32d6-49ea-b4eb-dcb9d1590c11", - DSN: jimmtest.CreateEmptyDatabase(c), - VaultAddress: "http://localhost:8200", - VaultAuthPath: "/auth/approle/login", - VaultPath: "/jimm-kv/", - VaultSecretFile: "./local/vault/approle.json", - OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), - OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), - SessionCookieMaxAge: 60, - }, - DashboardFinalRedirectURL: "", - } vaultClient, _, creds, _ := jimmtest.VaultClient(c, ".") - + p := jimmtest.NewTestJimmParams(c) + p.VaultAddress = "http://localhost:8200" + p.VaultAuthPath = "/auth/approle/login" + p.VaultPath = "/jimm-kv/" + p.VaultSecretFile = "./local/vault/approle.json" + p.OpenFGAParams = cofgaParamsToJIMMOpenFGAParams(*cofgaParams) svc, err := jimm.NewService(ctx, p) c.Assert(err, qt.IsNil) @@ -239,20 +194,9 @@ func TestPostgresSecretStore(t *testing.T) { _, _, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) - p := jimm.Params{ - ControllerUUID: "6acf4fd8-32d6-49ea-b4eb-dcb9d1590c11", - DSN: jimmtest.CreateEmptyDatabase(c), - OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), - InsecureSecretStorage: true, - OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), - SessionCookieMaxAge: 60, - }, - DashboardFinalRedirectURL: "", - } + p := jimmtest.NewTestJimmParams(c) + p.InsecureSecretStorage = true + p.OpenFGAParams = cofgaParamsToJIMMOpenFGAParams(*cofgaParams) _, err = jimm.NewService(context.Background(), p) c.Assert(err, qt.IsNil) } @@ -264,22 +208,10 @@ func TestOpenFGA(t *testing.T) { _, _, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) - p := jimm.Params{ - ControllerUUID: "6acf4fd8-32d6-49ea-b4eb-dcb9d1590c11", - DSN: jimmtest.CreateEmptyDatabase(c), - OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), - ControllerAdmins: []string{"alice", "eve"}, - InsecureSecretStorage: true, - OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), - SessionCookieMaxAge: 60, - }, - DashboardFinalRedirectURL: "", - } - + p := jimmtest.NewTestJimmParams(c) + p.InsecureSecretStorage = true + p.ControllerAdmins = []string{"alice", "eve"} + p.OpenFGAParams = cofgaParamsToJIMMOpenFGAParams(*cofgaParams) svc, err := jimm.NewService(ctx, p) c.Assert(err, qt.IsNil) @@ -324,22 +256,8 @@ func TestPublicKey(t *testing.T) { _, _, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) - p := jimm.Params{ - ControllerUUID: "6acf4fd8-32d6-49ea-b4eb-dcb9d1590c11", - DSN: jimmtest.CreateEmptyDatabase(c), - OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), - ControllerAdmins: []string{"alice", "eve"}, - PrivateKey: "c1VkV05+iWzCxMwMVcWbr0YJWQSEO62v+z3EQ2BhFMw=", - PublicKey: "pC8MEk9MS9S8fhyRnOJ4qARTcTAwoM9L1nH/Yq0MwWU=", - OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), - SessionCookieMaxAge: 60, - }, - DashboardFinalRedirectURL: "", - } + p := jimmtest.NewTestJimmParams(c) + p.OpenFGAParams = cofgaParamsToJIMMOpenFGAParams(*cofgaParams) svc, err := jimm.NewService(context.Background(), p) c.Assert(err, qt.IsNil) @@ -350,7 +268,7 @@ func TestPublicKey(t *testing.T) { c.Assert(err, qt.IsNil) data, err := io.ReadAll(response.Body) c.Assert(err, qt.IsNil) - c.Assert(string(data), qt.Equals, `{"PublicKey":"pC8MEk9MS9S8fhyRnOJ4qARTcTAwoM9L1nH/Yq0MwWU="}`) + c.Assert(string(data), qt.Equals, `{"PublicKey":"izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk="}`) } func TestThirdPartyCaveatDischarge(t *testing.T) { @@ -413,22 +331,8 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { ofgaClient, _, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) - p := jimm.Params{ - ControllerUUID: "6acf4fd8-32d6-49ea-b4eb-dcb9d1590c11", - DSN: jimmtest.CreateEmptyDatabase(c), - OpenFGAParams: cofgaParamsToJIMMOpenFGAParams(*cofgaParams), - ControllerAdmins: []string{"alice", "eve"}, - PrivateKey: "c1VkV05+iWzCxMwMVcWbr0YJWQSEO62v+z3EQ2BhFMw=", - PublicKey: "pC8MEk9MS9S8fhyRnOJ4qARTcTAwoM9L1nH/Yq0MwWU=", - OAuthAuthenticatorParams: jimm.OAuthAuthenticatorParams{ - IssuerURL: "http://localhost:8082/realms/jimm", - ClientID: "jimm-device", - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - SessionTokenExpiry: time.Duration(time.Hour), - SessionCookieMaxAge: 60, - }, - DashboardFinalRedirectURL: "", - } + p := jimmtest.NewTestJimmParams(c) + p.OpenFGAParams = cofgaParamsToJIMMOpenFGAParams(*cofgaParams) svc, err := jimm.NewService(context.Background(), p) c.Assert(err, qt.IsNil) From 494106897f6a3bb782522421dc55d657225dd19b Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Mon, 8 Apr 2024 09:32:26 +0200 Subject: [PATCH 103/126] Removed candid from machine charm (#1189) * Removed candid from machine charm * Fix env vars to add bakery_ prefix --- charms/jimm/config.yaml | 15 --- charms/jimm/src/charm.py | 27 +--- charms/jimm/templates/jimm.env | 10 +- charms/jimm/templates/jimm.service | 1 + charms/jimm/tests/test_charm.py | 195 ++++++++++------------------- 5 files changed, 70 insertions(+), 178 deletions(-) diff --git a/charms/jimm/config.yaml b/charms/jimm/config.yaml index d09278233..b03a8a42c 100644 --- a/charms/jimm/config.yaml +++ b/charms/jimm/config.yaml @@ -10,21 +10,6 @@ options: Logs are purged at 9AM UTC. Defaults to 0, which means by default logs are never purged. default: "0" - candid-agent-private-key: - type: string - description: | - Private key of the agent user for accessing the candid API. - candid-agent-public-key: - type: string - description: | - Public key of the agent user for accessing the candid API. - candid-agent-username: - type: string - description: | - Name of the agent user for accessing the candid API. - candid-url: - type: string - description: URL of candid server controller-admins: type: string description: | diff --git a/charms/jimm/src/charm.py b/charms/jimm/src/charm.py index 9d170ebd0..25dfefbdb 100755 --- a/charms/jimm/src/charm.py +++ b/charms/jimm/src/charm.py @@ -71,7 +71,6 @@ def __init__(self, *args): self.on.dashboard_relation_joined, self._on_dashboard_relation_joined, ) - self._agent_filename = "/var/snap/jimm/common/agent.json" self._vault_secret_filename = "/var/snap/jimm/common/vault_secret.json" self._workload_filename = "/snap/bin/jimm" self._rsyslog_conf_path = "/etc/rsyslog.d/10-jimm.conf" @@ -141,14 +140,12 @@ def _on_config_changed(self, _): args = { "admins": self.config.get("controller-admins", ""), - "bakery_agent_file": self._bakery_agent_file(), - "candid_url": self.config.get("candid-url"), "dns_name": self.config.get("dns-name"), "log_level": self.config.get("log-level"), "uuid": self.config.get("uuid"), "dashboard_location": self.config.get("juju-dashboard-location"), - "public_key": self.config.get("public-key"), - "private_key": self.config.get("private-key"), + "bakery_public_key": self.config.get("public-key", ""), + "bakery_private_key": self.config.get("private-key", ""), "audit_retention_period": self.config.get("audit-log-retention-period-in-days", ""), "jwt_expiry": self.config.get("jwt-expiry", "5m"), "macaroon_expiry_duration": self.config.get("macaroon-expiry-duration"), @@ -353,24 +350,6 @@ def _setup_logging(self): ) self._systemctl("restart", "rsyslog") - def _bakery_agent_file(self): - url = self.config.get("candid-url", "") - username = self.config.get("candid-agent-username", "") - private_key = self.config.get("candid-agent-private-key", "") - public_key = self.config.get("candid-agent-public-key", "") - if not url or not username or not private_key or not public_key: - return "" - data = { - "key": {"public": public_key, "private": private_key}, - "agents": [{"url": url, "username": username}], - } - try: - with open(self._agent_filename, "wt") as f: - json.dump(data, f) - except FileNotFoundError: - return "" - return self._agent_filename - def _write_service_file(self): args = { "conf_file": self._env_filename(), @@ -378,6 +357,7 @@ def _write_service_file(self): "leader_file": self._env_filename(LEADER_PART), "vault_file": self._env_filename(VAULT_PART), "openfga_file": self._env_filename(OPENFGA_PART), + "oauth_file": self._env_filename(OAUTH_PART), } with open(self.service_file, "wt") as f: f.write(self._render_template("jimm.service", **args)) @@ -429,7 +409,6 @@ def _update_dashboard_relation(self, relation: Relation): relation.data[self.app].update( { "controller_url": "wss://{}".format(self.config["dns-name"]), - "identity_provider_url": self.config["candid-url"], "is_juju": str(False), } ) diff --git a/charms/jimm/templates/jimm.env b/charms/jimm/templates/jimm.env index ab7e19584..714006d03 100644 --- a/charms/jimm/templates/jimm.env +++ b/charms/jimm/templates/jimm.env @@ -1,5 +1,3 @@ -BAKERY_AGENT_FILE={{bakery_agent_file}} -CANDID_URL={{candid_url}} JIMM_ADMINS={{admins}} {% if dashboard_location %} JIMM_DASHBOARD_LOCATION={{dashboard_location}} @@ -9,12 +7,8 @@ JIMM_DNS_NAME={{dns_name}} {% endif %} JIMM_LOG_LEVEL={{log_level}} JIMM_UUID={{uuid}} -{% if private_key %} -PRIVATE_KEY={{private_key}} -{% endif %} -{% if public_key %} -PUBLIC_KEY={{public_key}} -{% endif %} +BAKERY_PRIVATE_KEY={{bakery_private_key}} +BAKERY_PUBLIC_KEY={{bakery_public_key}} JIMM_AUDIT_LOG_RETENTION_PERIOD_IN_DAYS={{audit_retention_period}} {%- if insecure_secret_storage %} INSECURE_SECRET_STORAGE=enabled diff --git a/charms/jimm/templates/jimm.service b/charms/jimm/templates/jimm.service index 9fee2d9a8..1e932f01e 100644 --- a/charms/jimm/templates/jimm.service +++ b/charms/jimm/templates/jimm.service @@ -10,6 +10,7 @@ EnvironmentFile={{db_file}} EnvironmentFile=-{{leader_file}} EnvironmentFile=-{{vault_file}} EnvironmentFile=-{{openfga_file}} +EnvironmentFile=-{{oauth_file}} ExecStart=/snap/bin/jimm Restart=on-failure RestartSec=10s diff --git a/charms/jimm/tests/test_charm.py b/charms/jimm/tests/test_charm.py index e21c539a1..9a54b8d2c 100644 --- a/charms/jimm/tests/test_charm.py +++ b/charms/jimm/tests/test_charm.py @@ -68,14 +68,14 @@ def setUp(self): def add_oauth_relation(self): self.oauth_rel_id = self.harness.add_relation("oauth", "hydra") self.harness.add_relation_unit(self.oauth_rel_id, "hydra/0") - secret_id = self.harness.add_model_secret("hydra", {"secret": OAUTH_CLIENT_SECRET}) - self.harness.grant_secret(secret_id, "juju-jimm") + self.oauth_secret_id = self.harness.add_model_secret("hydra", {"secret": OAUTH_CLIENT_SECRET}) + self.harness.grant_secret(self.oauth_secret_id, "juju-jimm") self.harness.update_relation_data( self.oauth_rel_id, "hydra", { "client_id": OAUTH_CLIENT_ID, - "client_secret_id": secret_id, + "client_secret_id": self.oauth_secret_id, **OAUTH_PROVIDER_INFO, }, ) @@ -162,7 +162,6 @@ def test_config_changed(self): config_file = os.path.join(self.harness.charm.charm_dir, "juju-jimm.env") self.harness.update_config( { - "candid-url": "https://candid.example.com", "controller-admins": "user1 user2 group1", "dns-name": "jimm.example.com", "log-level": "debug", @@ -181,41 +180,38 @@ def test_config_changed(self): with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 25) - self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=") - self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com") - self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1") + self.assertEqual(len(lines), 19) + self.assertEqual(lines[0].strip(), "JIMM_ADMINS=user1 user2 group1") self.assertEqual( - lines[4].strip(), + lines[2].strip(), "JIMM_DASHBOARD_LOCATION=https://jaas.ai/models", ) - self.assertEqual(lines[7].strip(), "JIMM_DNS_NAME=" + "jimm.example.com") - self.assertEqual(lines[9].strip(), "JIMM_LOG_LEVEL=debug") - self.assertEqual(lines[10].strip(), "JIMM_UUID=caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa") + self.assertEqual(lines[5].strip(), "JIMM_DNS_NAME=" + "jimm.example.com") + self.assertEqual(lines[7].strip(), "JIMM_LOG_LEVEL=debug") + self.assertEqual(lines[8].strip(), "JIMM_UUID=caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa") self.assertEqual( - lines[12].strip(), - "PRIVATE_KEY=ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", + lines[9].strip(), + "BAKERY_PRIVATE_KEY=ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", ) self.assertEqual( - lines[15].strip(), - "PUBLIC_KEY=izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", + lines[10].strip(), + "BAKERY_PUBLIC_KEY=izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", ) self.assertEqual( - lines[17].strip(), + lines[11].strip(), "JIMM_AUDIT_LOG_RETENTION_PERIOD_IN_DAYS=10", ) self.assertEqual( - lines[18].strip(), + lines[12].strip(), "JIMM_JWT_EXPIRY=10m", ) - self.assertEqual(lines[20].strip(), "JIMM_MACAROON_EXPIRY_DURATION=48h") - self.assertEqual(lines[21].strip(), "JIMM_ACCESS_TOKEN_EXPIRY_DURATION=6h") + self.assertEqual(lines[14].strip(), "JIMM_MACAROON_EXPIRY_DURATION=48h") + self.assertEqual(lines[15].strip(), "JIMM_ACCESS_TOKEN_EXPIRY_DURATION=6h") def test_config_changed_redirect_to_dashboard(self): config_file = os.path.join(self.harness.charm.charm_dir, "juju-jimm.env") self.harness.update_config( { - "candid-url": "https://candid.example.com", "controller-admins": "user1 user2 group1", "dns-name": "jimm.example.com", "log-level": "debug", @@ -234,34 +230,32 @@ def test_config_changed_redirect_to_dashboard(self): with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 25) - self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=") - self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com") - self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1") + self.assertEqual(len(lines), 19) + self.assertEqual(lines[0].strip(), "JIMM_ADMINS=user1 user2 group1") self.assertEqual( - lines[4].strip(), + lines[2].strip(), "JIMM_DASHBOARD_LOCATION=https://test.jaas.ai/models", ) - self.assertEqual(lines[7].strip(), "JIMM_DNS_NAME=" + "jimm.example.com") - self.assertEqual(lines[9].strip(), "JIMM_LOG_LEVEL=debug") - self.assertEqual(lines[10].strip(), "JIMM_UUID=caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa") + self.assertEqual(lines[5].strip(), "JIMM_DNS_NAME=" + "jimm.example.com") + self.assertEqual(lines[7].strip(), "JIMM_LOG_LEVEL=debug") + self.assertEqual(lines[8].strip(), "JIMM_UUID=caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa") self.assertEqual( - lines[12].strip(), - "PRIVATE_KEY=ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", + lines[9].strip(), + "BAKERY_PRIVATE_KEY=ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", ) self.assertEqual( - lines[15].strip(), - "PUBLIC_KEY=izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", + lines[10].strip(), + "BAKERY_PUBLIC_KEY=izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", ) self.assertEqual( - lines[17].strip(), + lines[11].strip(), "JIMM_AUDIT_LOG_RETENTION_PERIOD_IN_DAYS=10", ) self.assertEqual( - lines[18].strip(), + lines[12].strip(), "JIMM_JWT_EXPIRY=5m", ) - self.assertEqual(lines[20].strip(), "JIMM_MACAROON_EXPIRY_DURATION=48h") + self.assertEqual(lines[14].strip(), "JIMM_MACAROON_EXPIRY_DURATION=48h") def test_config_changed_ready(self): config_file = os.path.join(self.harness.charm.charm_dir, "juju-jimm.env") @@ -269,7 +263,6 @@ def test_config_changed_ready(self): f.write("test") self.harness.update_config( { - "candid-url": "https://candid.example.com", "controller-admins": "user1 user2 group1", "uuid": "caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa", "public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", @@ -285,94 +278,31 @@ def test_config_changed_ready(self): with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 23) - self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=") - self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com") - self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1") + self.assertEqual(len(lines), 17) + self.assertEqual(lines[0].strip(), "JIMM_ADMINS=user1 user2 group1") self.assertEqual( - lines[4].strip(), + lines[2].strip(), "JIMM_DASHBOARD_LOCATION=https://jaas.ai/models", ) - self.assertEqual(lines[7].strip(), "JIMM_LOG_LEVEL=info") - self.assertEqual(lines[8].strip(), "JIMM_UUID=caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa") + self.assertEqual(lines[5].strip(), "JIMM_LOG_LEVEL=info") + self.assertEqual(lines[6].strip(), "JIMM_UUID=caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa") self.assertEqual( - lines[10].strip(), - "PRIVATE_KEY=ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", + lines[7].strip(), + "BAKERY_PRIVATE_KEY=ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", ) self.assertEqual( - lines[13].strip(), - "PUBLIC_KEY=izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", + lines[8].strip(), + "BAKERY_PUBLIC_KEY=izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", ) self.assertEqual( - lines[15].strip(), + lines[9].strip(), "JIMM_AUDIT_LOG_RETENTION_PERIOD_IN_DAYS=10", ) self.assertEqual( - lines[16].strip(), + lines[10].strip(), "JIMM_JWT_EXPIRY=5m", ) - self.assertEqual(lines[18].strip(), "JIMM_MACAROON_EXPIRY_DURATION=48h") - - def test_config_changed_with_agent(self): - config_file = os.path.join(self.harness.charm.charm_dir, "juju-jimm.env") - self.harness.charm._agent_filename = os.path.join(self.tempdir.name, "agent.json") - self.harness.update_config( - { - "candid-agent-username": "username@candid", - "candid-agent-private-key": "agent-private-key", - "candid-agent-public-key": "agent-public-key", - "candid-url": "https://candid.example.com", - "controller-admins": "user1 user2 group1", - "uuid": "caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa", - "public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", - "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", - } - ) - self.assertTrue(os.path.exists(self.harness.charm._agent_filename)) - with open(self.harness.charm._agent_filename) as f: - data = json.load(f) - self.assertEqual(data["key"]["public"], "agent-public-key") - self.assertEqual(data["key"]["private"], "agent-private-key") - - self.assertTrue(os.path.exists(config_file)) - - with open(config_file) as f: - lines = f.readlines() - self.assertEqual(len(lines), 23) - self.assertEqual( - lines[0].strip(), - "BAKERY_AGENT_FILE=" + self.harness.charm._agent_filename, - ) - self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com") - self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1") - self.assertEqual(lines[4].strip(), "JIMM_DASHBOARD_LOCATION=https://jaas.ai/models") - self.assertEqual(lines[7].strip(), "JIMM_LOG_LEVEL=info") - self.assertEqual(lines[8].strip(), "JIMM_UUID=caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa") - self.assertEqual(lines[18].strip(), "JIMM_MACAROON_EXPIRY_DURATION=24h") - - self.harness.charm._agent_filename = os.path.join(self.tempdir.name, "no-such-dir", "agent.json") - self.harness.update_config( - { - "candid-agent-username": "username@candid", - "candid-agent-private-key": "agent-private-key2", - "candid-agent-public-key": "agent-public-key2", - "candid-url": "https://candid.example.com", - "controller-admins": "user1 user2 group1", - "uuid": "caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa", - "public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", - "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", - } - ) - with open(config_file) as f: - lines = f.readlines() - self.assertEqual(len(lines), 23) - self.assertEqual(lines[0].strip(), "BAKERY_AGENT_FILE=") - self.assertEqual(lines[1].strip(), "CANDID_URL=https://candid.example.com") - self.assertEqual(lines[2].strip(), "JIMM_ADMINS=user1 user2 group1") - self.assertEqual(lines[4].strip(), "JIMM_DASHBOARD_LOCATION=https://jaas.ai/models") - self.assertEqual(lines[7].strip(), "JIMM_LOG_LEVEL=info") - self.assertEqual(lines[8].strip(), "JIMM_UUID=caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa") - self.assertEqual(lines[18].strip(), "JIMM_MACAROON_EXPIRY_DURATION=24h") + self.assertEqual(lines[12].strip(), "JIMM_MACAROON_EXPIRY_DURATION=48h") def test_leader_elected(self): leader_file = os.path.join(self.harness.charm.charm_dir, "juju-jimm-leader.env") @@ -590,28 +520,21 @@ def test_dashboard_relation_joined(self): harness.begin() harness.set_leader(True) - with tempfile.NamedTemporaryFile() as tmp: - harness.charm._agent_filename = tmp.name - harness.update_config( - { - "dns-name": "jimm.example.com", - "candid-agent-username": "username@candid", - "candid-agent-private-key": "agent-private-key", - "candid-agent-public-key": "agent-public-key", - "candid-url": "https://candid.example.com", - "controller-admins": "user1 user2 group1", - "uuid": "caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa", - "public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", - "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", - } - ) + harness.update_config( + { + "dns-name": "jimm.example.com", + "controller-admins": "user1 user2 group1", + "uuid": "caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa", + "public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", + "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", + } + ) id = harness.add_relation("dashboard", "juju-dashboard") harness.add_relation_unit(id, "juju-dashboard/0") data = harness.get_relation_data(id, "juju-jimm") self.assertTrue(data) self.assertEqual(data["controller_url"], "wss://jimm.example.com") - self.assertEqual(data["identity_provider_url"], "https://candid.example.com") self.assertEqual(data["is_juju"], "False") def test_openfga_relation_changed(self): @@ -650,7 +573,6 @@ def test_insecure_secret_storage(self): config_file = os.path.join(self.harness.charm.charm_dir, "juju-jimm.env") self.harness.update_config( { - "candid-url": "https://candid.example.com", "controller-admins": "user1 user2 group1", "dns-name": "jimm.example.com", "log-level": "debug", @@ -663,16 +585,27 @@ def test_insecure_secret_storage(self): with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 25) + self.assertEqual(len(lines), 19) self.assertEqual(len([match for match in lines if "INSECURE_SECRET_STORAGE" in match]), 0) self.harness.update_config({"postgres-secret-storage": True}) self.assertTrue(os.path.exists(config_file)) with open(config_file) as f: lines = f.readlines() os.unlink(config_file) - self.assertEqual(len(lines), 27) + self.assertEqual(len(lines), 21) self.assertEqual(len([match for match in lines if "INSECURE_SECRET_STORAGE" in match]), 1) + def test_oauth_relation_changed(self): + self.harness.set_leader(True) + self.add_oauth_relation() + + with open(self.harness.charm._env_filename("oauth")) as f: + lines = f.readlines() + self.assertEqual(lines[0].strip(), "JIMM_OAUTH_ISSUER_URL=https://example.oidc.com") + self.assertEqual(lines[1].strip(), "JIMM_OAUTH_CLIENT_ID=jimm_client_id") + self.assertEqual(lines[2].strip(), "JIMM_OAUTH_CLIENT_SECRET=test-secret") + self.assertEqual(lines[3].strip(), "JIMM_OAUTH_SCOPES=openid profile email phone") + class VersionHTTPRequestHandler(BaseHTTPRequestHandler): def __init__(self, *args, **kwargs): From 0a8fa50e3686b321aac2949601e057b6355ff1e9 Mon Sep 17 00:00:00 2001 From: alesstimec Date: Wed, 10 Apr 2024 09:55:20 +0200 Subject: [PATCH 104/126] Various OIDC related fixes. --- docker-compose.yaml | 5 ----- internal/auth/oauth2.go | 2 +- local/traefik/traefik.yaml | 3 --- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index f62dfe6ff..94d441933 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,11 +21,6 @@ services: interval: 10s timeout: 5s retries: 3 - labels: - traefik.enable: true - traefik.http.routers.traefik.rule: Host(`127.0.0.1`) - traefik.http.routers.traefik.entrypoints: websecure - traefik.http.routers.traefik.tls: true jimm: image: cosmtrek/air:latest diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 8217c957a..4eee890f0 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -359,7 +359,7 @@ func VerifySessionToken(token string, secretKey string) (jwt.Token, error) { parsedToken, err := jwt.Parse(decodedToken, jwt.WithKey(jwa.HS256, []byte(secretKey))) if err != nil { if stderrors.Is(err, jwt.ErrTokenExpired()) { - return nil, errors.E(op, "JIMM session token expired") + return nil, errors.E(op, errors.CodeUnauthorized, "JIMM session token expired") } return nil, errors.E(op, err) } diff --git a/local/traefik/traefik.yaml b/local/traefik/traefik.yaml index 00585c0e5..93e335ce0 100644 --- a/local/traefik/traefik.yaml +++ b/local/traefik/traefik.yaml @@ -46,9 +46,6 @@ tls: certificates: - certFile: /certs/server.crt keyFile: /certs/server.key - default: - keyFile: /certs/server.key - certFile: /certs/server.crt # when troubleshooting certs, enable this so traefik doesn't use # its own self-signed. By default if it can't find a matching # cert, it'll just create its own which will cause cert warnings From 0490107a51c3d9aebf0e2fac726f7d1aadd7e8e6 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 11 Apr 2024 08:54:08 +0100 Subject: [PATCH 105/126] Add `@serviceaccount` domain to service accounts (#1190) * Ignore `jimmsrv` binary Signed-off-by: Babak K. Shandiz * Update `IsValidServiceAccountId` to verify domain Signed-off-by: Babak K. Shandiz * Add `ensureValidClientIdWithDomain` function Signed-off-by: Babak K. Shandiz * Ensure client ID has a domain when passing to JIMM Signed-off-by: Babak K. Shandiz * Update tests with `@serviceaccount` domain Signed-off-by: Babak K. Shandiz * Replace `@canonical.com` with `@serviceaccount` Signed-off-by: Babak K. Shandiz * Append `@serviceaccount` to client ID at login Signed-off-by: Babak K. Shandiz * Remove `@canonical` from test client ID Signed-off-by: Babak K. Shandiz * Fix `TestVerifyClientCredentials` Signed-off-by: Babak K. Shandiz * Fix `TestLoginWithClientCredentials` Signed-off-by: Babak K. Shandiz * Add `EnsureValidClientIdWithDomain` function Signed-off-by: Babak K. Shandiz * Update tests with latest `IsValidServiceAccountId` changes Signed-off-by: Babak K. Shandiz * Add tests for `EnsureValidServiceAccountIdWithDomain` Signed-off-by: Babak K. Shandiz * Rename method to `EnsureValidServiceAccountId` Signed-off-by: Babak K. Shandiz * Ensure proper cloud credential tags Signed-off-by: Babak K. Shandiz * Update `jaas` CLI tests Signed-off-by: Babak K. Shandiz * Use `EnsureValidServiceAccountId` to append domain Signed-off-by: Babak K. Shandiz * Use `EnsureValidServiceAccountId` in `names` package instead of local Signed-off-by: Babak K. Shandiz * Update tests with `@serviceaccount` domain Signed-off-by: Babak K. Shandiz * Improve error message for unsupported local users Signed-off-by: Babak K. Shandiz * Update tests with latest error message Signed-off-by: Babak K. Shandiz --------- Signed-off-by: Babak K. Shandiz Co-authored-by: Ales Stimec --- .gitignore | 1 + cmd/jaas/cmd/addserviceaccount_test.go | 5 +- cmd/jaas/cmd/grant_test.go | 11 +- .../cmd/listserviceaccountcredentials_test.go | 9 +- cmd/jaas/cmd/updatecredentials.go | 13 +- cmd/jaas/cmd/updatecredentials_test.go | 34 +-- internal/auth/oauth2_test.go | 2 +- internal/jimm/service_account_test.go | 8 +- internal/jujuapi/admin.go | 10 +- internal/jujuapi/admin_test.go | 4 +- internal/jujuapi/controllerroot.go | 3 +- internal/jujuapi/modelmanager_test.go | 2 +- internal/jujuapi/service_account.go | 29 ++- internal/jujuapi/service_account_test.go | 196 ++++++++++++++---- internal/jujuapi/usermanager_test.go | 2 +- local/keycloak/jimm-realm.json | 2 +- pkg/names/service_account.go | 26 ++- pkg/names/service_account_test.go | 73 ++++++- 18 files changed, 327 insertions(+), 103 deletions(-) diff --git a/.gitignore b/.gitignore index 3f6eb2d60..1e63a1908 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ local/vault/roleid.txt *.key *.csr /jimmctl +/jimmsrv qa-controller /cloudinit.temp.yaml diff --git a/cmd/jaas/cmd/addserviceaccount_test.go b/cmd/jaas/cmd/addserviceaccount_test.go index 565ae5d53..62f34a7e7 100644 --- a/cmd/jaas/cmd/addserviceaccount_test.go +++ b/cmd/jaas/cmd/addserviceaccount_test.go @@ -24,7 +24,8 @@ type addServiceAccountSuite struct { var _ = gc.Suite(&addServiceAccountSuite{}) func (s *addServiceAccountSuite) TestAddServiceAccount(c *gc.C) { - clientID := "abda51b2-d735-4794-a8bd-49c506baa4af@canonical.com" + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + clientIDWithDomain := clientID + "@serviceaccount" // alice is superuser bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err := cmdtesting.RunCommand(c, cmd.NewAddServiceAccountCommandForTesting(s.ClientStore(), bClient), clientID) @@ -32,7 +33,7 @@ func (s *addServiceAccountSuite) TestAddServiceAccount(c *gc.C) { tuple := openfga.Tuple{ Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientIDWithDomain)), } // Check alice has access. ok, err := s.JIMM.OpenFGAClient.CheckRelation(context.Background(), tuple, false) diff --git a/cmd/jaas/cmd/grant_test.go b/cmd/jaas/cmd/grant_test.go index 7dd1503de..38206e856 100644 --- a/cmd/jaas/cmd/grant_test.go +++ b/cmd/jaas/cmd/grant_test.go @@ -27,13 +27,14 @@ var _ = gc.Suite(&grantSuite{}) func (s *grantSuite) TestGrant(c *gc.C) { ctx := context.Background() - clientID := "abda51b2-d735-4794-a8bd-49c506baa4af@canonical.com" + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + clientIdWithDomain := clientID + "@serviceaccount" // alice is superuser bClient := jimmtest.NewUserSessionLogin(c, "alice") sa := dbmodel.Identity{ - Name: clientID, + Name: clientIdWithDomain, } err := s.JIMM.Database.GetIdentity(ctx, &sa) c.Assert(err, gc.IsNil) @@ -42,7 +43,7 @@ func (s *grantSuite) TestGrant(c *gc.C) { tuple := openfga.Tuple{ Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientIdWithDomain)), } err = s.JIMM.OpenFGAClient.AddRelation(ctx, tuple) c.Assert(err, gc.IsNil) @@ -57,7 +58,7 @@ func (s *grantSuite) TestGrant(c *gc.C) { ok, err := s.JIMM.OpenFGAClient.CheckRelation(ctx, openfga.Tuple{ Object: ofganames.ConvertTag(names.NewUserTag("bob")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientIdWithDomain)), }, false) c.Assert(err, gc.IsNil) c.Assert(ok, gc.Equals, true) @@ -65,7 +66,7 @@ func (s *grantSuite) TestGrant(c *gc.C) { ok, err = s.JIMM.OpenFGAClient.CheckRelation(ctx, openfga.Tuple{ Object: ofganames.ConvertTag(jimmnames.NewGroupTag("1#member")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientIdWithDomain)), }, false) c.Assert(err, gc.IsNil) c.Assert(ok, gc.Equals, true) diff --git a/cmd/jaas/cmd/listserviceaccountcredentials_test.go b/cmd/jaas/cmd/listserviceaccountcredentials_test.go index 0a7c9c4c8..590c2deb0 100644 --- a/cmd/jaas/cmd/listserviceaccountcredentials_test.go +++ b/cmd/jaas/cmd/listserviceaccountcredentials_test.go @@ -34,20 +34,21 @@ func (s *listServiceAccountCredentialsSuite) TestListServiceAccountCredentials(c }) c.Assert(err, gc.IsNil) // Create Alice Identity and Service Account Identity. - clientID := "abda51b2-d735-4794-a8bd-49c506baa4af@canonical.com" + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + clientIDWithDomain := clientID + "@serviceaccount" // alice is superuser ctx := context.Background() user := dbmodel.Identity{Name: "alice@canonical.com"} u := openfga.NewUser(&user, s.OFGAClient) - err = s.JIMM.AddServiceAccount(ctx, u, clientID) + err = s.JIMM.AddServiceAccount(ctx, u, clientIDWithDomain) c.Assert(err, gc.IsNil) - svcAcc := dbmodel.Identity{Name: clientID} + svcAcc := dbmodel.Identity{Name: clientIDWithDomain} err = s.JIMM.Database.GetIdentity(ctx, &svcAcc) c.Assert(err, gc.IsNil) svcAccIdentity := openfga.NewUser(&svcAcc, s.OFGAClient) // Create cloud-credential for service account. updateArgs := jimm.UpdateCloudCredentialArgs{ - CredentialTag: names.NewCloudCredentialTag(fmt.Sprintf("aws/%s/foo", clientID)), + CredentialTag: names.NewCloudCredentialTag(fmt.Sprintf("aws/%s/foo", clientIDWithDomain)), Credential: params.CloudCredential{Attributes: map[string]string{"foo": "bar"}}, } _, err = s.JIMM.UpdateCloudCredential(ctx, svcAccIdentity, updateArgs) diff --git a/cmd/jaas/cmd/updatecredentials.go b/cmd/jaas/cmd/updatecredentials.go index 0d100ddfa..80eed83cf 100644 --- a/cmd/jaas/cmd/updatecredentials.go +++ b/cmd/jaas/cmd/updatecredentials.go @@ -18,6 +18,7 @@ import ( "github.com/canonical/jimm/api" apiparams "github.com/canonical/jimm/api/params" "github.com/canonical/jimm/internal/errors" + jimmnames "github.com/canonical/jimm/pkg/names" ) var ( @@ -110,8 +111,18 @@ func (c *updateCredentialsCommand) Run(ctxt *cmd.Context) error { return errors.E(err) } + // Note that ensuring a client ID comes with the correct domain (which is + // `@serviceaccount`) is not the responsibility of the CLI commands and is + // actually taken care of in the `jujuapi` package. But, here, since we need + // to create cloud credential tags, which are meant to be used by JIMM + // internals, we have to make sure they're in the correct format. + clientIdWithDomain, err := jimmnames.EnsureValidServiceAccountId(c.clientID) + if err != nil { + return errors.E("invalid client ID") + } + taggedCredential := jujuparams.TaggedCredential{ - Tag: names.NewCloudCredentialTag(fmt.Sprintf("%s/%s/%s", c.cloud, c.clientID, c.credentialName)).String(), + Tag: names.NewCloudCredentialTag(fmt.Sprintf("%s/%s/%s", c.cloud, clientIdWithDomain, c.credentialName)).String(), Credential: *credential, } diff --git a/cmd/jaas/cmd/updatecredentials_test.go b/cmd/jaas/cmd/updatecredentials_test.go index b03e7e6a8..691e446fb 100644 --- a/cmd/jaas/cmd/updatecredentials_test.go +++ b/cmd/jaas/cmd/updatecredentials_test.go @@ -28,13 +28,14 @@ var _ = gc.Suite(&updateCredentialsSuite{}) func (s *updateCredentialsSuite) TestUpdateCredentialsWithNewCredentials(c *gc.C) { ctx := context.Background() - clientID := "abda51b2-d735-4794-a8bd-49c506baa4af@canonical.com" + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + clientIDWithDomain := clientID + "@serviceaccount" // alice is superuser bClient := jimmtest.NewUserSessionLogin(c, "alice") sa := dbmodel.Identity{ - Name: clientID, + Name: clientIDWithDomain, } err := s.JIMM.Database.GetIdentity(ctx, &sa) c.Assert(err, gc.IsNil) @@ -43,7 +44,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithNewCredentials(c *gc.C tuple := openfga.Tuple{ Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientIDWithDomain)), } err = s.JIMM.OpenFGAClient.AddRelation(ctx, tuple) c.Assert(err, gc.IsNil) @@ -69,13 +70,13 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithNewCredentials(c *gc.C cmdContext, err := cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(clientStore, bClient), clientID, "test-cloud", "test-credentials") c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(cmdContext), gc.Equals, `results: -- credentialtag: cloudcred-test-cloud_abda51b2-d735-4794-a8bd-49c506baa4af@canonical.com_test-credentials +- credentialtag: cloudcred-test-cloud_abda51b2-d735-4794-a8bd-49c506baa4af@serviceaccount_test-credentials error: null models: [] `) ofgaUser := openfga.NewUser(&sa, s.JIMM.AuthorizationClient()) - cloudCredentialTag := names.NewCloudCredentialTag("test-cloud/" + clientID + "/test-credentials") + cloudCredentialTag := names.NewCloudCredentialTag("test-cloud/" + clientIDWithDomain + "/test-credentials") cloudCredential2, err := s.JIMM.GetCloudCredential(ctx, ofgaUser, cloudCredentialTag) c.Assert(err, gc.IsNil) attrs, _, err := s.JIMM.GetCloudCredentialAttributes(ctx, ofgaUser, cloudCredential2, true) @@ -89,13 +90,14 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithNewCredentials(c *gc.C func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c *gc.C) { ctx := context.Background() - clientID := "abda51b2-d735-4794-a8bd-49c506baa4af@canonical.com" + clientID := "abda51b2-d735-4794-a8bd-49c506baa4af" + clientIDWithDomain := clientID + "@serviceaccount" // alice is superuser bClient := jimmtest.NewUserSessionLogin(c, "alice") sa := dbmodel.Identity{ - Name: clientID, + Name: clientIDWithDomain, } err := s.JIMM.Database.GetIdentity(ctx, &sa) c.Assert(err, gc.IsNil) @@ -104,7 +106,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c tuple := openfga.Tuple{ Object: ofganames.ConvertTag(names.NewUserTag("alice@canonical.com")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientID)), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientIDWithDomain)), } err = s.JIMM.OpenFGAClient.AddRelation(ctx, tuple) c.Assert(err, gc.IsNil) @@ -119,7 +121,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c cloudCredential := dbmodel.CloudCredential{ Name: "test-credentials", CloudName: "test-cloud", - OwnerIdentityName: clientID, + OwnerIdentityName: clientIDWithDomain, AuthType: "empty", } err = s.JIMM.Database.SetCloudCredential(ctx, &cloudCredential) @@ -139,13 +141,13 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c cmdContext, err := cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(clientStore, bClient), clientID, "test-cloud", "test-credentials") c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(cmdContext), gc.Equals, `results: -- credentialtag: cloudcred-test-cloud_abda51b2-d735-4794-a8bd-49c506baa4af@canonical.com_test-credentials +- credentialtag: cloudcred-test-cloud_abda51b2-d735-4794-a8bd-49c506baa4af@serviceaccount_test-credentials error: null models: [] `) ofgaUser := openfga.NewUser(&sa, s.JIMM.AuthorizationClient()) - cloudCredentialTag := names.NewCloudCredentialTag("test-cloud/" + clientID + "/test-credentials") + cloudCredentialTag := names.NewCloudCredentialTag("test-cloud/" + clientIDWithDomain + "/test-credentials") cloudCredential2, err := s.JIMM.GetCloudCredential(ctx, ofgaUser, cloudCredentialTag) c.Assert(err, gc.IsNil) attrs, _, err := s.JIMM.GetCloudCredentialAttributes(ctx, ofgaUser, cloudCredential2, true) @@ -159,7 +161,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c func (s *updateCredentialsSuite) TestCloudNotInLocalStore(c *gc.C) { bClient := jimmtest.NewUserSessionLogin(c, "alice") _, err := cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(s.ClientStore(), bClient), - "00000000-0000-0000-0000-000000000000@canonical.com", + "00000000-0000-0000-0000-000000000000", "non-existing-cloud", "foo", ) @@ -178,7 +180,7 @@ func (s *updateCredentialsSuite) TestCredentialNotInLocalStore(c *gc.C) { c.Assert(err, gc.IsNil) _, err = cmdtesting.RunCommand(c, cmd.NewUpdateCredentialsCommandForTesting(clientStore, bClient), - "00000000-0000-0000-0000-000000000000@canonical.com", + "00000000-0000-0000-0000-000000000000", "some-cloud", "non-existing-credential-name", ) @@ -196,15 +198,15 @@ func (s *updateCredentialsSuite) TestMissingArgs(c *gc.C) { expectedError: "client ID not specified", }, { name: "missing cloud", - args: []string{"some-client-id@canonical.com"}, + args: []string{"some-client-id"}, expectedError: "cloud not specified", }, { name: "missing credential name", - args: []string{"some-client-id@canonical.com", "some-cloud"}, + args: []string{"some-client-id", "some-cloud"}, expectedError: "credential name not specified", }, { name: "too many args", - args: []string{"some-client-id@canonical.com", "some-cloud", "some-credential-name", "extra-arg"}, + args: []string{"some-client-id", "some-cloud", "some-credential-name", "extra-arg"}, expectedError: "too many args", }} diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index dcf3e9edd..634ecd882 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -245,7 +245,7 @@ func TestVerifyClientCredentials(t *testing.T) { const ( // these are valid client credentials hardcoded into the jimm realm - validClientID = "test-client-id@canonical.com" + validClientID = "test-client-id" validClientSecret = "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf" ) diff --git a/internal/jimm/service_account_test.go b/internal/jimm/service_account_test.go index 302ba3222..12bcec6cf 100644 --- a/internal/jimm/service_account_test.go +++ b/internal/jimm/service_account_test.go @@ -35,7 +35,7 @@ func TestAddServiceAccount(t *testing.T) { }, client, ) - clientID := "39caae91-b914-41ae-83f8-c7b86ca5ad5a@canonical.com" + clientID := "39caae91-b914-41ae-83f8-c7b86ca5ad5a@serviceaccount" err = j.AddServiceAccount(ctx, user, clientID) c.Assert(err, qt.IsNil) err = j.AddServiceAccount(ctx, user, clientID) @@ -73,7 +73,7 @@ func TestGrantServiceAccountAccess(t *testing.T) { "user-bob", "group-1#member", }, - clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", username: "alice", }, { about: "Group that doesn't exist", @@ -86,7 +86,7 @@ func TestGrantServiceAccountAccess(t *testing.T) { // This group doesn't exist. "group-bar", }, - clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", username: "alice", expectedError: "group bar not found", }, { @@ -99,7 +99,7 @@ func TestGrantServiceAccountAccess(t *testing.T) { "user-bob", "controller-jimm", }, - clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", username: "alice", expectedError: "invalid entity - not user or group", }} diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index 81a143f4e..2cd1d0eaf 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -16,6 +16,7 @@ import ( "github.com/canonical/jimm/internal/errors" "github.com/canonical/jimm/internal/jimm" "github.com/canonical/jimm/internal/openfga" + jimmnames "github.com/canonical/jimm/pkg/names" ) // unsupportedLogin returns an appropriate error for login attempts using @@ -174,16 +175,21 @@ func (r *controllerRoot) LoginWithSessionToken(ctx context.Context, req params.L func (r *controllerRoot) LoginWithClientCredentials(ctx context.Context, req params.LoginWithClientCredentialsRequest) (jujuparams.LoginResult, error) { const op = errors.Op("jujuapi.LoginWithClientCredentials") + clientIdWithDomain, err := jimmnames.EnsureValidServiceAccountId(req.ClientID) + if err != nil { + return jujuparams.LoginResult{}, errors.E("invalid client ID") + } + authenticationSvc := r.jimm.OAuthAuthenticationService() if authenticationSvc == nil { return jujuparams.LoginResult{}, errors.E("authentication service not specified") } - err := authenticationSvc.VerifyClientCredentials(ctx, req.ClientID, req.ClientSecret) + err = authenticationSvc.VerifyClientCredentials(ctx, req.ClientID, req.ClientSecret) if err != nil { return jujuparams.LoginResult{}, errors.E(err, errors.CodeUnauthorized) } - user, err := r.jimm.GetOpenFGAUserAndAuthorise(ctx, req.ClientID) + user, err := r.jimm.GetOpenFGAUserAndAuthorise(ctx, clientIdWithDomain) if err != nil { return jujuparams.LoginResult{}, errors.E(op, err) } diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index 4f6a827ee..514fc6769 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -305,7 +305,7 @@ func (s *adminSuite) TestLoginWithClientCredentials(c *gc.C) { const ( // these are valid client credentials hardcoded into the jimm realm - validClientID = "test-client-id@canonical.com" + validClientID = "test-client-id" validClientSecret = "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf" ) @@ -316,7 +316,7 @@ func (s *adminSuite) TestLoginWithClientCredentials(c *gc.C) { }, &loginResult) c.Assert(err, gc.IsNil) c.Assert(loginResult.ControllerTag, gc.Equals, names.NewControllerTag(s.Params.ControllerUUID).String()) - c.Assert(loginResult.UserInfo.Identity, gc.Equals, names.NewUserTag("test-client-id@canonical.com").String()) + c.Assert(loginResult.UserInfo.Identity, gc.Equals, names.NewUserTag("test-client-id@serviceaccount").String()) err = conn.APICall("Admin", 4, "", "LoginWithClientCredentials", params.LoginWithClientCredentialsRequest{ ClientID: "invalid-client-id", diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index 6ba3a55be..ac8b84a8e 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -4,6 +4,7 @@ package jujuapi import ( "context" + "fmt" "sync" "time" @@ -194,7 +195,7 @@ func parseUserTag(tag string) (names.UserTag, error) { return names.UserTag{}, errors.E(errors.CodeBadRequest, err) } if ut.IsLocal() { - return names.UserTag{}, errors.E(errors.CodeBadRequest, "unsupported local user") + return names.UserTag{}, errors.E(errors.CodeBadRequest, fmt.Sprintf("unsupported local user; if this is a service account add @%s domain", jimmnames.ServiceAccountDomain)) } return ut, nil } diff --git a/internal/jujuapi/modelmanager_test.go b/internal/jujuapi/modelmanager_test.go index 640dccaba..f2f9e5c16 100644 --- a/internal/jujuapi/modelmanager_test.go +++ b/internal/jujuapi/modelmanager_test.go @@ -1000,7 +1000,7 @@ func (s *modelManagerSuite) TestModifyModelAccessErrors(c *gc.C) { Access: jujuparams.ModelReadAccess, ModelTag: s.Model.Tag().String(), }, - expectError: `unsupported local user`, + expectError: `unsupported local user; if this is a service account add @serviceaccount domain`, }, { about: "no such model", modifyModelAccess: jujuparams.ModifyModelAccess{ diff --git a/internal/jujuapi/service_account.go b/internal/jujuapi/service_account.go index 9b9806f01..2609f8744 100644 --- a/internal/jujuapi/service_account.go +++ b/internal/jujuapi/service_account.go @@ -16,27 +16,33 @@ import ( jimmnames "github.com/canonical/jimm/pkg/names" ) -// service_acount contains the primary RPC commands for handling service accounts within JIMM via the JIMM facade itself. +// service_account contains the primary RPC commands for handling service accounts within JIMM via the JIMM facade itself. // AddGroup creates a group within JIMMs DB for reference by OpenFGA. func (r *controllerRoot) AddServiceAccount(ctx context.Context, req apiparams.AddServiceAccountRequest) error { const op = errors.Op("jujuapi.AddServiceAccount") - if !jimmnames.IsValidServiceAccountId(req.ClientID) { - return errors.E(op, errors.CodeBadRequest, "invalid client ID") + clientIdWithDomain, err := jimmnames.EnsureValidServiceAccountId(req.ClientID) + if err != nil { + return errors.E(op, errors.CodeBadRequest, err) } - return r.jimm.AddServiceAccount(ctx, r.user, req.ClientID) + return r.jimm.AddServiceAccount(ctx, r.user, clientIdWithDomain) } // getServiceAccount validates the incoming identity has administrator permission // on the service account and returns the service account identity. func (r *controllerRoot) getServiceAccount(ctx context.Context, clientID string) (*openfga.User, error) { - if !jimmnames.IsValidServiceAccountId(clientID) { + clientIdWithDomain, err := jimmnames.EnsureValidServiceAccountId(clientID) + if err != nil { + return nil, errors.E(errors.CodeBadRequest, err) + } + + if !jimmnames.IsValidServiceAccountId(clientIdWithDomain) { return nil, errors.E(errors.CodeBadRequest, "invalid client ID") } - ok, err := r.user.IsServiceAccountAdmin(ctx, jimmnames.NewServiceAccountTag(clientID)) + ok, err := r.user.IsServiceAccountAdmin(ctx, jimmnames.NewServiceAccountTag(clientIdWithDomain)) if err != nil { return nil, errors.E(err) } @@ -45,7 +51,7 @@ func (r *controllerRoot) getServiceAccount(ctx context.Context, clientID string) } var targetIdentityModel dbmodel.Identity - targetIdentityModel.SetTag(names.NewUserTag(clientID)) + targetIdentityModel.SetTag(names.NewUserTag(clientIdWithDomain)) if err := r.jimm.DB().GetIdentity(ctx, &targetIdentityModel); err != nil { return nil, errors.E(err) } @@ -110,11 +116,16 @@ func (r *controllerRoot) ListServiceAccountCredentials(ctx context.Context, req func (r *controllerRoot) GrantServiceAccountAccess(ctx context.Context, req apiparams.GrantServiceAccountAccess) error { const op = errors.Op("jujuapi.GrantServiceAccountAccess") - _, err := r.getServiceAccount(ctx, req.ClientID) + clientIdWithDomain, err := jimmnames.EnsureValidServiceAccountId(req.ClientID) + if err != nil { + return errors.E(op, errors.CodeBadRequest, err) + } + + _, err = r.getServiceAccount(ctx, clientIdWithDomain) if err != nil { return errors.E(op, err) } - svcAccTag := jimmnames.NewServiceAccountTag(req.ClientID) + svcAccTag := jimmnames.NewServiceAccountTag(clientIdWithDomain) return r.jimm.GrantServiceAccountAccess(ctx, r.user, svcAccTag, req.Entities) } diff --git a/internal/jujuapi/service_account_test.go b/internal/jujuapi/service_account_test.go index 531ab2935..638ac42b5 100644 --- a/internal/jujuapi/service_account_test.go +++ b/internal/jujuapi/service_account_test.go @@ -28,34 +28,50 @@ func TestAddServiceAccount(t *testing.T) { c := qt.New(t) tests := []struct { - about string - addServiceAccount func(ctx context.Context, user *openfga.User, clientID string) error - args params.AddServiceAccountRequest - expectedError string + about string + args params.AddServiceAccountRequest + addedClientId string + expectedError string }{{ - about: "Valid client ID", - addServiceAccount: func(ctx context.Context, user *openfga.User, clientID string) error { - return nil + about: "Valid client ID without domain", + args: params.AddServiceAccountRequest{ + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", }, + addedClientId: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", + }, { + about: "Valid client ID with correct domain", args: params.AddServiceAccountRequest{ - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", }, + addedClientId: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", }, { - about: "Invalid Client ID", - addServiceAccount: func(ctx context.Context, user *openfga.User, clientID string) error { - return nil + about: "Valid client ID with wrong domain", + args: params.AddServiceAccountRequest{ + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@not-serviceaccount", }, + expectedError: "invalid client ID", + }, { + about: "Invalid Client ID without domain", args: params.AddServiceAccountRequest{ ClientID: "_123_", }, expectedError: "invalid client ID", + }, { + about: "Invalid Client ID with wrong domain", + args: params.AddServiceAccountRequest{ + ClientID: "_123_@not-serviceaccount", + }, + expectedError: "invalid client ID", }} for _, test := range tests { test := test c.Run(test.about, func(c *qt.C) { jimm := &jimmtest.JIMM{ - AddServiceAccount_: test.addServiceAccount, + AddServiceAccount_: func(_ context.Context, _ *openfga.User, clientID string) error { + c.Assert(clientID, qt.Equals, test.addedClientId) + return nil + }, } cr := jujuapi.NewControllerRoot(jimm, jujuapi.Params{}) @@ -73,24 +89,41 @@ func TestGetServiceAccount(t *testing.T) { c := qt.New(t) tests := []struct { - about string - clientID string - addTuples []openfga.Tuple - username string - expectedError string + about string + clientID string + addTuples []openfga.Tuple + username string + expectedClientID string + expectedError string }{{ - about: "Valid request", - clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", + about: "Valid request without domain", + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + username: "alice", + addTuples: []openfga.Tuple{{ + Object: ofganames.ConvertTag(names.NewUserTag("alice")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount")), + }}, + expectedClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", + }, { + about: "Valid request with domain", + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", username: "alice", addTuples: []openfga.Tuple{{ Object: ofganames.ConvertTag(names.NewUserTag("alice")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com")), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount")), }}, + expectedClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", + }, { + about: "Invalid request: wrong domain", + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@not-serviceaccount", + username: "alice", + expectedError: "invalid client ID", }, { about: "Missing service account administrator permission", username: "alice", - clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", + clientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", expectedError: "unauthorized", }, { about: "Invalid Client ID", @@ -126,7 +159,7 @@ func TestGetServiceAccount(t *testing.T) { res, err := cr.GetServiceAccount(context.Background(), test.clientID) if test.expectedError == "" { c.Assert(err, qt.IsNil) - c.Assert(res.Identity.Name, qt.Equals, test.clientID) + c.Assert(res.Identity.Name, qt.Equals, test.expectedClientID) } else { c.Assert(err, qt.ErrorMatches, test.expectedError) } @@ -146,7 +179,7 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { expectedResult jujuparams.UpdateCredentialResults expectedError string }{{ - about: "Valid request", + about: "Valid request without domain", updateCloudCredential: func(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) { return nil, nil }, @@ -164,7 +197,7 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { }, }}, args: params.UpdateServiceAccountCredentialsRequest{ - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ Credentials: []jujuparams.TaggedCredential{ { @@ -181,7 +214,45 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { addTuples: []openfga.Tuple{{ Object: ofganames.ConvertTag(names.NewUserTag("alice")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com")), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount")), + }}, + }, { + about: "Valid request with domain", + updateCloudCredential: func(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) { + return nil, nil + }, + expectedResult: jujuparams.UpdateCredentialResults{ + Results: []jujuparams.UpdateCredentialResult{ + { + CredentialTag: "cloudcred-aws/1cbe5066-ea80-4979-8633-048d32f46cf8/cred-name", + Error: nil, + Models: nil, + }, + { + CredentialTag: "cloudcred-azure/1cbe5066-ea80-4979-8633-048d32f46cf8/cred-name2", + Error: nil, + Models: nil, + }, + }}, + args: params.UpdateServiceAccountCredentialsRequest{ + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", + UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ + Credentials: []jujuparams.TaggedCredential{ + { + Tag: "cloudcred-aws/1cbe5066-ea80-4979-8633-048d32f46cf8/cred-name", + Credential: jujuparams.CloudCredential{Attributes: map[string]string{"foo": "bar"}}, + }, + { + Tag: "cloudcred-azure/1cbe5066-ea80-4979-8633-048d32f46cf8/cred-name2", + Credential: jujuparams.CloudCredential{Attributes: map[string]string{"wolf": "low"}}, + }, + }}, + }, + username: "alice", + addTuples: []openfga.Tuple{{ + Object: ofganames.ConvertTag(names.NewUserTag("alice")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount")), }}, }, { about: "Invalid Credential Tag", @@ -199,7 +270,7 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { }, }}, args: params.UpdateServiceAccountCredentialsRequest{ - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ Credentials: []jujuparams.TaggedCredential{ { @@ -212,7 +283,7 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { addTuples: []openfga.Tuple{{ Object: ofganames.ConvertTag(names.NewUserTag("alice")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com")), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount")), }}, }, { about: "Invalid Service account ID", @@ -237,7 +308,7 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { return nil, nil }, args: params.UpdateServiceAccountCredentialsRequest{ - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ Credentials: []jujuparams.TaggedCredential{ { @@ -300,14 +371,14 @@ func TestListServiceAccountCredentials(t *testing.T) { expectedResult jujuparams.CredentialContentResults expectedError string }{{ - about: "Valid request", + about: "Valid request without domain", ForEachUserCloudCredential: func(ctx context.Context, u *dbmodel.Identity, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error { return nil }, expectedResult: jujuparams.CredentialContentResults{ Results: []jujuparams.CredentialContentResult{}}, args: params.ListServiceAccountCredentialsRequest{ - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", }, getCloudCredential: func(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) { cred := &dbmodel.CloudCredential{} @@ -320,7 +391,30 @@ func TestListServiceAccountCredentials(t *testing.T) { addTuples: []openfga.Tuple{{ Object: ofganames.ConvertTag(names.NewUserTag("alice")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com")), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount")), + }}, + }, { + about: "Valid request with domain", + ForEachUserCloudCredential: func(ctx context.Context, u *dbmodel.Identity, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error { + return nil + }, + expectedResult: jujuparams.CredentialContentResults{ + Results: []jujuparams.CredentialContentResult{}}, + args: params.ListServiceAccountCredentialsRequest{ + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", + }, + getCloudCredential: func(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) { + cred := &dbmodel.CloudCredential{} + return cred, nil + }, + getCloudCredentialAttributes: func(ctx context.Context, u *openfga.User, cred *dbmodel.CloudCredential, hidden bool) (attrs map[string]string, redacted []string, err error) { + return nil, nil, nil + }, + username: "alice", + addTuples: []openfga.Tuple{{ + Object: ofganames.ConvertTag(names.NewUserTag("alice")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount")), }}, }, { about: "Invalid Service account ID", @@ -345,7 +439,7 @@ func TestListServiceAccountCredentials(t *testing.T) { return nil }, args: params.ListServiceAccountCredentialsRequest{ - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", }, getCloudCredential: func(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) { cred := &dbmodel.CloudCredential{} @@ -408,7 +502,25 @@ func TestGrantServiceAccountAccess(t *testing.T) { addTuples []openfga.Tuple expectedError string }{{ - about: "Valid request", + about: "Valid request without domain", + grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, svcAccTag jimmnames.ServiceAccountTag, entities []string) error { + return nil + }, + params: params.GrantServiceAccountAccess{ + Entities: []string{ + "user-alice", + "user-bob", + }, + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be", + }, + username: "alice", + addTuples: []openfga.Tuple{{ + Object: ofganames.ConvertTag(names.NewUserTag("alice")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount")), + }}, + }, { + about: "Valid request with domain", grantServiceAccountAccess: func(ctx context.Context, user *openfga.User, svcAccTag jimmnames.ServiceAccountTag, entities []string) error { return nil }, @@ -417,13 +529,13 @@ func TestGrantServiceAccountAccess(t *testing.T) { "user-alice", "user-bob", }, - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", }, username: "alice", addTuples: []openfga.Tuple{{ Object: ofganames.ConvertTag(names.NewUserTag("alice")), Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com")), + Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount")), }}, }, { about: "Invalid Service account ID", @@ -449,7 +561,7 @@ func TestGrantServiceAccountAccess(t *testing.T) { "user-alice", "user-bob", }, - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", }, username: "alice", expectedError: "unauthorized", @@ -501,7 +613,7 @@ func (s *serviceAccountSuite) TestUpdateServiceAccountCredentialsIntegration(c * conn := s.open(c, nil, "bob") defer conn.Close() - serviceAccount := jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com") + serviceAccount := jimmnames.NewServiceAccountTag("fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount") tuple := openfga.Tuple{ Object: ofganames.ConvertTag(names.NewUserTag("bob@canonical.com")), @@ -517,15 +629,15 @@ func (s *serviceAccountSuite) TestUpdateServiceAccountCredentialsIntegration(c * var credResults jujuparams.UpdateCredentialResults err := conn.APICall("JIMM", 4, "", "UpdateServiceAccountCredentials", params.UpdateServiceAccountCredentialsRequest{ - ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com", + ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ Credentials: []jujuparams.TaggedCredential{ { - Tag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com/cred-name", + Tag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount/cred-name", Credential: jujuparams.CloudCredential{Attributes: map[string]string{"foo": "bar"}}, }, { - Tag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com/cred-name2", + Tag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount/cred-name2", Credential: jujuparams.CloudCredential{Attributes: map[string]string{"wolf": "low"}}, }, }}, @@ -534,12 +646,12 @@ func (s *serviceAccountSuite) TestUpdateServiceAccountCredentialsIntegration(c * expectedResult := jujuparams.UpdateCredentialResults{ Results: []jujuparams.UpdateCredentialResult{ { - CredentialTag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com/cred-name", + CredentialTag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount/cred-name", Error: nil, Models: nil, }, { - CredentialTag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be@canonical.com/cred-name2", + CredentialTag: "cloudcred-aws/fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount/cred-name2", Error: nil, Models: nil, }, diff --git a/internal/jujuapi/usermanager_test.go b/internal/jujuapi/usermanager_test.go index 9d3aa0f21..bd322aa1e 100644 --- a/internal/jujuapi/usermanager_test.go +++ b/internal/jujuapi/usermanager_test.go @@ -125,7 +125,7 @@ func (s *userManagerSuite) TestUserInfoLocalUsername(c *gc.C) { client := usermanager.NewClient(conn) users, err := client.UserInfo([]string{"alice"}, usermanager.AllUsers) - c.Assert(err, gc.ErrorMatches, `alice: unsupported local user`) + c.Assert(err, gc.ErrorMatches, `alice: unsupported local user; if this is a service account add @serviceaccount domain`) c.Assert(users, gc.HasLen, 0) } diff --git a/local/keycloak/jimm-realm.json b/local/keycloak/jimm-realm.json index 980dea6af..ae1e5b0cf 100644 --- a/local/keycloak/jimm-realm.json +++ b/local/keycloak/jimm-realm.json @@ -693,7 +693,7 @@ ] }, { - "clientId": "test-client-id@canonical.com", + "clientId": "test-client-id", "name": "", "description": "", "rootUrl": "", diff --git a/pkg/names/service_account.go b/pkg/names/service_account.go index ae6c2c424..e4575c7fc 100644 --- a/pkg/names/service_account.go +++ b/pkg/names/service_account.go @@ -6,7 +6,9 @@ package names import ( "fmt" + "strings" + "github.com/canonical/jimm/internal/errors" "github.com/juju/names/v5" ) @@ -14,6 +16,10 @@ const ( // ServiceAccountTagKind represents the resource "kind" that service accounts // are represented as. ServiceAccountTagKind = "serviceaccount" + + // ServiceAccountDomain is the @domain suffix that service account IDs should + // have. + ServiceAccountDomain = "serviceaccount" ) // ServiceAccount represents a service account where id is the client ID. @@ -54,11 +60,27 @@ func ParseServiceAccountTag(tag string) (ServiceAccountTag, error) { return gt, nil } -// IsValidServiceAccountId verifies the client id for a service account is valid according to a regex internally. +// IsValidServiceAccountId verifies the client id for a service account is valid +// according to a regex internally. A valid service account ID must have a +// `@serviceaccount` domain. func IsValidServiceAccountId(id string) bool { if !names.IsValidUser(id) { return false } t := names.NewUserTag(id) - return t.Domain() != "" + return t.Domain() == ServiceAccountDomain +} + +// EnsureValidServiceAccountId returns the given service account ID with the +// `@serviceaccount` appended to it, if not already there. If the ID is not a +// valid service account ID this function returns an error. +func EnsureValidServiceAccountId(id string) (string, error) { + if !strings.HasSuffix(id, "@"+ServiceAccountDomain) { + id += "@" + ServiceAccountDomain + } + + if !IsValidServiceAccountId(id) { + return "", errors.E(errors.CodeBadRequest, "invalid client ID") + } + return id, nil } diff --git a/pkg/names/service_account_test.go b/pkg/names/service_account_test.go index 156fb8869..445c2a3ca 100644 --- a/pkg/names/service_account_test.go +++ b/pkg/names/service_account_test.go @@ -3,6 +3,7 @@ package names import ( "testing" + qt "github.com/frankban/quicktest" "github.com/stretchr/testify/assert" ) @@ -14,8 +15,8 @@ func TestParseServiceAccountID(t *testing.T) { err string }{{ about: "Valid svc account tag", - tag: "serviceaccount-1e654457-a195-4a41-8360-929c7f455d43@canonical.com", - expectedID: "1e654457-a195-4a41-8360-929c7f455d43@canonical.com", + tag: "serviceaccount-1e654457-a195-4a41-8360-929c7f455d43@serviceaccount", + expectedID: "1e654457-a195-4a41-8360-929c7f455d43@serviceaccount", err: "", }, { about: "Invalid svc account tag (no domain)", @@ -23,11 +24,11 @@ func TestParseServiceAccountID(t *testing.T) { err: "is not a valid serviceaccount tag", }, { about: "Invalid svc account tag (serviceaccounts)", - tag: "serviceaccounts-1e654457-a195-4a41-8360-929c7f455d43@canonical.com", + tag: "serviceaccounts-1e654457-a195-4a41-8360-929c7f455d43@serviceaccount", err: "is not a valid tag", }, { about: "Invalid svc account tag (no prefix)", - tag: "1e654457-a195-4a41-8360-929c7f455d43@canonical.com", + tag: "1e654457-a195-4a41-8360-929c7f455d43@serviceaccount", err: "is not a valid tag", }, { about: "Invalid svc account tag (missing ID)", @@ -51,13 +52,67 @@ func TestParseServiceAccountID(t *testing.T) { } func TestIsValidServiceAccountId(t *testing.T) { - assert.True(t, IsValidServiceAccountId("1e654457-a195-4a41-8360-929c7f455d43@canonical.com")) - assert.True(t, IsValidServiceAccountId("12345@canonical.com")) - assert.True(t, IsValidServiceAccountId("abc123@canonical.com")) - assert.True(t, IsValidServiceAccountId("ABC123@canonical.com")) - assert.True(t, IsValidServiceAccountId("ABC123@canonical.com")) + assert.True(t, IsValidServiceAccountId("1e654457-a195-4a41-8360-929c7f455d43@serviceaccount")) + assert.True(t, IsValidServiceAccountId("12345@serviceaccount")) + assert.True(t, IsValidServiceAccountId("abc123@serviceaccount")) + assert.True(t, IsValidServiceAccountId("ABC123@serviceaccount")) + assert.True(t, IsValidServiceAccountId("ABC123@serviceaccount")) assert.False(t, IsValidServiceAccountId("ABC123")) assert.False(t, IsValidServiceAccountId("abc 123")) assert.False(t, IsValidServiceAccountId("")) assert.False(t, IsValidServiceAccountId(" ")) + assert.False(t, IsValidServiceAccountId("@")) + assert.False(t, IsValidServiceAccountId("@serviceaccount")) + assert.False(t, IsValidServiceAccountId("abc123@some-other-domain")) + assert.False(t, IsValidServiceAccountId("abc123@")) +} + +func TestEnsureValidClientIdWithDomain(t *testing.T) { + c := qt.New(t) + + tests := []struct { + name string + id string + expectedError bool + expectedId string + }{{ + name: "uuid, no domain", + id: "00000000-0000-0000-0000-000000000000", + expectedId: "00000000-0000-0000-0000-000000000000@serviceaccount", + }, { + name: "uuid, with domain", + id: "00000000-0000-0000-0000-000000000000@serviceaccount", + expectedId: "00000000-0000-0000-0000-000000000000@serviceaccount", + }, { + name: "empty", + id: "", + expectedError: true, + }, { + name: "empty id, with correct domain", + id: "@serviceaccount", + expectedError: true, + }, { + name: "uuid, with wrong domain", + id: "00000000-0000-0000-0000-000000000000@some-domain", + expectedError: true, + }, { + name: "invalid format", + id: "_123_", + expectedError: true, + }, + } + + for _, t := range tests { + tt := t + c.Run(tt.name, func(c *qt.C) { + result, err := EnsureValidServiceAccountId(tt.id) + if tt.expectedError { + c.Assert(err, qt.ErrorMatches, "invalid client ID") + c.Assert(result, qt.Equals, "") + } else { + c.Assert(err, qt.IsNil) + c.Assert(result, qt.Equals, tt.expectedId) + } + }) + } } From 7d9ed88baa4d701db5468e3994f6cddc36146716 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:32:58 +0100 Subject: [PATCH 106/126] =?UTF-8?q?feat(sanitise=20identity=20ids):=20iden?= =?UTF-8?q?tity=20Ids=20cannot=20contain=20underscores,=E2=80=A6=20(#1192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(sanitise identity ids): identity Ids cannot contain underscores, this replaces them with hyphen In addition to the underscores, we additionally set all unsafe email characters to hyphens too for added safety. As a side fix, the display name setting has been moved to the domain layer as it feels very mcuh like domain logic. 7419 * Set display name only if not set to prevent updating 10000 tests * Handle underscores at domain layer * Fix TestUserInfoWithDomain * pr comments * change comment --- cmd/jaas/cmd/grant_test.go | 8 +- .../cmd/listserviceaccountcredentials_test.go | 12 +- cmd/jaas/cmd/updatecredentials_test.go | 18 +- cmd/jimmctl/cmd/addcloudtocontroller_test.go | 13 +- cmd/jimmctl/cmd/relation_test.go | 18 +- internal/auth/oauth2.go | 32 +-- internal/auth/oauth2_test.go | 53 ++-- internal/cmdtest/jimmsuite.go | 35 ++- internal/db/applicationoffer_test.go | 11 +- internal/db/cloudcredential_test.go | 20 +- internal/db/clouddefaults_test.go | 17 +- internal/db/controller_test.go | 11 +- internal/db/db_test.go | 5 +- internal/db/identity_test.go | 114 +++++--- internal/db/identitymodeldefaults_test.go | 43 ++- internal/db/model.go | 3 + internal/db/model_test.go | 60 +++-- internal/dbmodel/cloudcredential_test.go | 14 +- internal/dbmodel/controller_test.go | 9 +- internal/dbmodel/identity.go | 67 +++++ internal/dbmodel/identity_test.go | 108 +++++--- internal/dbmodel/model.go | 7 +- internal/dbmodel/model_test.go | 24 +- internal/discharger/discharger.go | 8 +- internal/jimm/access_test.go | 43 +-- internal/jimm/applicationoffer.go | 43 ++- internal/jimm/applicationoffer_test.go | 244 ++++++++---------- internal/jimm/cloud.go | 16 +- internal/jimm/cloud_test.go | 58 +++-- internal/jimm/cloudcredential_test.go | 163 ++++++------ internal/jimm/clouddefaults_test.go | 115 ++++----- internal/jimm/controller.go | 9 +- internal/jimm/controller_test.go | 63 +++-- internal/jimm/identitymodeldefaults_test.go | 58 ++--- internal/jimm/jimm_test.go | 14 +- internal/jimm/model.go | 6 +- internal/jimm/model_test.go | 13 +- internal/jimm/service_account_test.go | 16 +- internal/jimm/user.go | 23 +- internal/jimm/user_test.go | 4 +- internal/jimmhttp/auth_handler_test.go | 7 +- internal/jimmtest/auth.go | 14 +- internal/jimmtest/env.go | 12 +- internal/jimmtest/keycloak.go | 9 + internal/jimmtest/suite.go | 45 ++-- internal/jujuapi/access_control_test.go | 10 +- internal/jujuapi/admin_test.go | 40 ++- internal/jujuapi/applicationoffers_test.go | 5 +- internal/jujuapi/cloud_test.go | 12 +- internal/jujuapi/jimm_test.go | 33 +-- internal/jujuapi/modelmanager_test.go | 11 +- internal/jujuapi/usermanager_test.go | 2 +- internal/jujuapi/websocket_test.go | 7 +- internal/openfga/user.go | 6 +- internal/openfga/user_test.go | 103 ++++++-- internal/rpc/proxy_test.go | 8 +- local/keycloak/jimm-realm.json | 22 ++ local/seed_db/main.go | 6 +- service.go | 6 +- service_test.go | 11 +- 60 files changed, 1154 insertions(+), 813 deletions(-) diff --git a/cmd/jaas/cmd/grant_test.go b/cmd/jaas/cmd/grant_test.go index 38206e856..a9399f00b 100644 --- a/cmd/jaas/cmd/grant_test.go +++ b/cmd/jaas/cmd/grant_test.go @@ -33,10 +33,10 @@ func (s *grantSuite) TestGrant(c *gc.C) { // alice is superuser bClient := jimmtest.NewUserSessionLogin(c, "alice") - sa := dbmodel.Identity{ - Name: clientIdWithDomain, - } - err := s.JIMM.Database.GetIdentity(ctx, &sa) + sa, err := dbmodel.NewIdentity(clientIdWithDomain) + c.Assert(err, gc.IsNil) + + err = s.JIMM.Database.GetIdentity(ctx, sa) c.Assert(err, gc.IsNil) // Make alice admin of the service account diff --git a/cmd/jaas/cmd/listserviceaccountcredentials_test.go b/cmd/jaas/cmd/listserviceaccountcredentials_test.go index 590c2deb0..9f781c9cb 100644 --- a/cmd/jaas/cmd/listserviceaccountcredentials_test.go +++ b/cmd/jaas/cmd/listserviceaccountcredentials_test.go @@ -38,14 +38,16 @@ func (s *listServiceAccountCredentialsSuite) TestListServiceAccountCredentials(c clientIDWithDomain := clientID + "@serviceaccount" // alice is superuser ctx := context.Background() - user := dbmodel.Identity{Name: "alice@canonical.com"} - u := openfga.NewUser(&user, s.OFGAClient) + user, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, gc.IsNil) + u := openfga.NewUser(user, s.OFGAClient) err = s.JIMM.AddServiceAccount(ctx, u, clientIDWithDomain) c.Assert(err, gc.IsNil) - svcAcc := dbmodel.Identity{Name: clientIDWithDomain} - err = s.JIMM.Database.GetIdentity(ctx, &svcAcc) + svcAcc, err := dbmodel.NewIdentity(clientIDWithDomain) + c.Assert(err, gc.IsNil) + err = s.JIMM.Database.GetIdentity(ctx, svcAcc) c.Assert(err, gc.IsNil) - svcAccIdentity := openfga.NewUser(&svcAcc, s.OFGAClient) + svcAccIdentity := openfga.NewUser(svcAcc, s.OFGAClient) // Create cloud-credential for service account. updateArgs := jimm.UpdateCloudCredentialArgs{ CredentialTag: names.NewCloudCredentialTag(fmt.Sprintf("aws/%s/foo", clientIDWithDomain)), diff --git a/cmd/jaas/cmd/updatecredentials_test.go b/cmd/jaas/cmd/updatecredentials_test.go index 691e446fb..b5446fced 100644 --- a/cmd/jaas/cmd/updatecredentials_test.go +++ b/cmd/jaas/cmd/updatecredentials_test.go @@ -34,10 +34,9 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithNewCredentials(c *gc.C // alice is superuser bClient := jimmtest.NewUserSessionLogin(c, "alice") - sa := dbmodel.Identity{ - Name: clientIDWithDomain, - } - err := s.JIMM.Database.GetIdentity(ctx, &sa) + sa, err := dbmodel.NewIdentity(clientIDWithDomain) + c.Assert(err, gc.IsNil) + err = s.JIMM.Database.GetIdentity(ctx, sa) c.Assert(err, gc.IsNil) // Make alice admin of the service account @@ -75,7 +74,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithNewCredentials(c *gc.C models: [] `) - ofgaUser := openfga.NewUser(&sa, s.JIMM.AuthorizationClient()) + ofgaUser := openfga.NewUser(sa, s.JIMM.AuthorizationClient()) cloudCredentialTag := names.NewCloudCredentialTag("test-cloud/" + clientIDWithDomain + "/test-credentials") cloudCredential2, err := s.JIMM.GetCloudCredential(ctx, ofgaUser, cloudCredentialTag) c.Assert(err, gc.IsNil) @@ -96,10 +95,9 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c // alice is superuser bClient := jimmtest.NewUserSessionLogin(c, "alice") - sa := dbmodel.Identity{ - Name: clientIDWithDomain, - } - err := s.JIMM.Database.GetIdentity(ctx, &sa) + sa, err := dbmodel.NewIdentity(clientIDWithDomain) + c.Assert(err, gc.IsNil) + err = s.JIMM.Database.GetIdentity(ctx, sa) c.Assert(err, gc.IsNil) // Make alice admin of the service account @@ -146,7 +144,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithExistingCredentials(c models: [] `) - ofgaUser := openfga.NewUser(&sa, s.JIMM.AuthorizationClient()) + ofgaUser := openfga.NewUser(sa, s.JIMM.AuthorizationClient()) cloudCredentialTag := names.NewCloudCredentialTag("test-cloud/" + clientIDWithDomain + "/test-credentials") cloudCredential2, err := s.JIMM.GetCloudCredential(ctx, ofgaUser, cloudCredentialTag) c.Assert(err, gc.IsNil) diff --git a/cmd/jimmctl/cmd/addcloudtocontroller_test.go b/cmd/jimmctl/cmd/addcloudtocontroller_test.go index 8e6e38374..f17f1a856 100644 --- a/cmd/jimmctl/cmd/addcloudtocontroller_test.go +++ b/cmd/jimmctl/cmd/addcloudtocontroller_test.go @@ -33,10 +33,9 @@ func (s *addCloudToControllerSuite) SetUpTest(c *gc.C) { s.JimmCmdSuite.SetUpTest(c) // We add user bob, who is a JIMM administrator. - err := s.JIMM.Database.UpdateIdentity(context.Background(), &dbmodel.Identity{ - DisplayName: "Bob", - Name: "bob@canonical.com", - }) + i, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, gc.IsNil) + err = s.JIMM.Database.UpdateIdentity(context.Background(), i) c.Assert(err, gc.IsNil) // We add a test-cloud cloud. @@ -52,10 +51,10 @@ func (s *addCloudToControllerSuite) SetUpTest(c *gc.C) { // We grant user bob administrator access to JIMM and the added // test-cloud. + i, err = dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, gc.IsNil) bob := openfga.NewUser( - &dbmodel.Identity{ - Name: "bob@canonical.com", - }, + i, s.JIMM.OpenFGAClient, ) err = bob.SetControllerAccess(context.Background(), s.JIMM.ResourceTag(), ofganames.AdministratorRelation) diff --git a/cmd/jimmctl/cmd/relation_test.go b/cmd/jimmctl/cmd/relation_test.go index 4c9bcd752..c240b2a23 100644 --- a/cmd/jimmctl/cmd/relation_test.go +++ b/cmd/jimmctl/cmd/relation_test.go @@ -268,12 +268,11 @@ type environment struct { func initializeEnvironment(c *gc.C, ctx context.Context, db *db.Database, u dbmodel.Identity) *environment { env := environment{} - u1 := dbmodel.Identity{ - Name: "eve@canonical.com", - } - c.Assert(db.DB.Create(&u1).Error, gc.IsNil) + u1, err := dbmodel.NewIdentity("eve@canonical.com") + c.Assert(err, gc.IsNil) + c.Assert(db.DB.Create(u1).Error, gc.IsNil) - env.users = []dbmodel.Identity{u, u1} + env.users = []dbmodel.Identity{u, *u1} cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -297,7 +296,7 @@ func initializeEnvironment(c *gc.C, ctx context.Context, db *db.Database, u dbmo CloudRegionID: cloud.Regions[0].ID, }}, } - err := db.AddController(ctx, &controller) + err = db.AddController(ctx, &controller) c.Assert(err, gc.Equals, nil) env.controllers = []dbmodel.Controller{controller} @@ -450,10 +449,9 @@ func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { err = db.GetGroup(ctx, &group) c.Assert(err, gc.IsNil) - u := dbmodel.Identity{ - Name: petname.Generate(2, "-") + "@canonical.com", - } - c.Assert(db.DB.Create(&u).Error, gc.IsNil) + u, err := dbmodel.NewIdentity(petname.Generate(2, "-") + "@canonical.com") + c.Assert(err, gc.IsNil) + c.Assert(db.DB.Create(u).Error, gc.IsNil) cloud := dbmodel.Cloud{ Name: petname.Generate(2, "-"), diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 4eee890f0..aa3e70bed 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -15,7 +15,6 @@ import ( "fmt" "net/http" "net/mail" - "strings" "time" "github.com/coreos/go-oidc/v3/oidc" @@ -305,9 +304,13 @@ func (as *AuthenticationService) UpdateIdentity(ctx context.Context, email strin const op = errors.Op("auth.UpdateIdentity") db := as.db - u := &dbmodel.Identity{ - Name: email, + + // TODO(ale8k): Add test case for this + u, err := dbmodel.NewIdentity(email) + if err != nil { + return errors.E(op, err) } + // TODO(babakks): If user does not exist, we will create one with an empty // display name (which we shouldn't). So it would be better to fetch // and then create. At the moment, GetUser is used for both create and fetch, @@ -316,15 +319,6 @@ func (as *AuthenticationService) UpdateIdentity(ctx context.Context, email strin if err := db.GetIdentity(ctx, u); err != nil { return errors.E(op, err) } - // Check if user has a display name, if not, set one - if u.DisplayName == "" { - splitEmail := strings.Split(email, "@") - if len(splitEmail) > 0 { - u.DisplayName = strings.Split(email, "@")[0] - } else { - return errors.E(op, "failed to split email") - } - } u.AccessToken = token.AccessToken u.RefreshToken = token.RefreshToken @@ -507,8 +501,10 @@ func (as *AuthenticationService) Whoami(ctx context.Context) (*params.WhoamiResp return nil, errors.E(op, "no identity in context") } - u := &dbmodel.Identity{ - Name: identityId, + // TODO(ale8k) CSS-8227: Add test case for this + u, err := dbmodel.NewIdentity(identityId) + if err != nil { + return nil, errors.E(op, err) } if err := as.db.GetIdentity(ctx, u); err != nil { @@ -533,9 +529,13 @@ func (as *AuthenticationService) validateAndUpdateAccessToken(ctx context.Contex } db := as.db - u := &dbmodel.Identity{ - Name: emailStr, + + // TODO(ale8k) CSS-8228: Add test case for this + u, err := dbmodel.NewIdentity(emailStr) + if err != nil { + return errors.E(op, err) } + if err := db.GetIdentity(ctx, u); err != nil { return errors.E(op, err) } diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 634ecd882..1281b058b 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -163,9 +163,8 @@ func TestDevice(t *testing.T) { err = authSvc.UpdateIdentity(ctx, email, token) c.Assert(err, qt.IsNil) - updatedUser := &dbmodel.Identity{ - Name: u.Email, - } + updatedUser, err := dbmodel.NewIdentity(u.Email) + c.Assert(err, qt.IsNil) c.Assert(db.GetIdentity(ctx, updatedUser), qt.IsNil) c.Assert(updatedUser.AccessToken, qt.Not(qt.Equals), "") c.Assert(updatedUser.RefreshToken, qt.Not(qt.Equals), "") @@ -308,7 +307,12 @@ func TestAuthenticateBrowserSessionAndLogout(t *testing.T) { authSvc, db, sessionStore := setupTestAuthSvc(ctx, c, time.Hour) - cookie, err := jimmtest.RunBrowserLogin(db, sessionStore) + cookie, err := jimmtest.RunBrowserLogin( + db, + sessionStore, + jimmtest.HardcodedSafeUsername, + jimmtest.HardcodedSafePassword, + ) c.Assert(err, qt.IsNil) rec := httptest.NewRecorder() @@ -349,7 +353,12 @@ func TestAuthenticateBrowserSessionRejectsNoneDecryptableOrDecodableCookies(t *t authSvc, db, sessionStore := setupTestAuthSvc(ctx, c, time.Hour) - _, err := jimmtest.RunBrowserLogin(db, sessionStore) + _, err := jimmtest.RunBrowserLogin( + db, + sessionStore, + jimmtest.HardcodedSafeUsername, + jimmtest.HardcodedSafePassword, + ) c.Assert(err, qt.IsNil) // Failure case 1: Bad base64 decoding @@ -386,7 +395,12 @@ func TestAuthenticateBrowserSessionHandlesExpiredAccessTokens(t *testing.T) { authSvc, db, sessionStore := setupTestAuthSvc(ctx, c, time.Hour) - cookie, err := jimmtest.RunBrowserLogin(db, sessionStore) + cookie, err := jimmtest.RunBrowserLogin( + db, + sessionStore, + jimmtest.HardcodedSafeUsername, + jimmtest.HardcodedSafePassword, + ) c.Assert(err, qt.IsNil) rec := httptest.NewRecorder() @@ -399,16 +413,15 @@ func TestAuthenticateBrowserSessionHandlesExpiredAccessTokens(t *testing.T) { // User exists from run browser login, but we're gonna // artificially expire their access token - u := dbmodel.Identity{ - Name: "jimm-test@canonical.com", - } - err = db.GetIdentity(ctx, &u) + u, err := dbmodel.NewIdentity("jimm-test@canonical.com") + c.Assert(err, qt.IsNil) + err = db.GetIdentity(ctx, u) c.Assert(err, qt.IsNil) previousToken := u.AccessToken u.AccessTokenExpiry = time.Now() - db.UpdateIdentity(ctx, &u) + db.UpdateIdentity(ctx, u) ctx, err = authSvc.AuthenticateBrowserSession(ctx, rec, req) c.Assert(err, qt.IsNil) @@ -418,7 +431,7 @@ func TestAuthenticateBrowserSessionHandlesExpiredAccessTokens(t *testing.T) { c.Assert(identityId, qt.Equals, "jimm-test@canonical.com") // Get identity again with new access token expiry and access token - err = db.GetIdentity(ctx, &u) + err = db.GetIdentity(ctx, u) c.Assert(err, qt.IsNil) // Assert new access token is valid for at least 4 minutes(our setup is 5 minutes) @@ -437,7 +450,12 @@ func TestAuthenticateBrowserSessionHandlesMissingOrExpiredRefreshTokens(t *testi authSvc, db, sessionStore := setupTestAuthSvc(ctx, c, time.Hour) - cookie, err := jimmtest.RunBrowserLogin(db, sessionStore) + cookie, err := jimmtest.RunBrowserLogin( + db, + sessionStore, + jimmtest.HardcodedSafeUsername, + jimmtest.HardcodedSafePassword, + ) c.Assert(err, qt.IsNil) rec := httptest.NewRecorder() @@ -450,10 +468,9 @@ func TestAuthenticateBrowserSessionHandlesMissingOrExpiredRefreshTokens(t *testi // User exists from run browser login, but we're gonna // artificially expire their access token - u := dbmodel.Identity{ - Name: "jimm-test@canonical.com", - } - err = db.GetIdentity(ctx, &u) + u, err := dbmodel.NewIdentity("jimm-test@canonical.com") + c.Assert(err, qt.IsNil) + err = db.GetIdentity(ctx, u) c.Assert(err, qt.IsNil) // As our access token has "expired" @@ -461,7 +478,7 @@ func TestAuthenticateBrowserSessionHandlesMissingOrExpiredRefreshTokens(t *testi // And we're missing a refresh token (the same case would apply for an expired refresh token // or any scenario where the token source cannot refresh the access token) u.RefreshToken = "" - db.UpdateIdentity(ctx, &u) + db.UpdateIdentity(ctx, u) // AuthenticateBrowserSession should fail to refresh the users session and delete // the current session, giving us the same cookie back with a max-age of -1. diff --git a/internal/cmdtest/jimmsuite.go b/internal/cmdtest/jimmsuite.go index 5ccdb7f4c..7c3f4b7e4 100644 --- a/internal/cmdtest/jimmsuite.go +++ b/internal/cmdtest/jimmsuite.go @@ -100,10 +100,11 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { } s.JujuConnSuite.SetUpTest(c) - s.AdminUser = &dbmodel.Identity{ - Name: "alice@canonical.com", - LastLogin: db.Now(), - } + i, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, gc.IsNil) + s.AdminUser = i + s.AdminUser.LastLogin = db.Now() + err = s.JIMM.Database.GetIdentity(ctx, s.AdminUser) c.Assert(err, gc.Equals, nil) @@ -186,12 +187,11 @@ func setupTLS(c *gc.C) *tls.Config { } func (s *JimmCmdSuite) AddAdminUser(c *gc.C, email string) { - identity := dbmodel.Identity{ - Name: email, - } - err := s.JIMM.Database.GetIdentity(context.Background(), &identity) + identity, err := dbmodel.NewIdentity(email) c.Assert(err, gc.IsNil) - ofgaUser := openfga.NewUser(&identity, s.OFGAClient) + err = s.JIMM.Database.GetIdentity(context.Background(), identity) + c.Assert(err, gc.IsNil) + ofgaUser := openfga.NewUser(identity, s.OFGAClient) err = ofgaUser.SetControllerAccess(context.Background(), s.JIMM.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, gc.IsNil) } @@ -235,11 +235,10 @@ func (s *JimmCmdSuite) AddController(c *gc.C, name string, info *api.Info) { func (s *JimmCmdSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, cred jujuparams.CloudCredential) { ctx := context.Background() - u := dbmodel.Identity{ - Name: tag.Owner().Id(), - } - user := openfga.NewUser(&u, s.JIMM.OpenFGAClient) - err := s.JIMM.Database.GetIdentity(ctx, &u) + u, err := dbmodel.NewIdentity(tag.Owner().Id()) + c.Assert(err, gc.IsNil) + user := openfga.NewUser(u, s.JIMM.OpenFGAClient) + err = s.JIMM.Database.GetIdentity(ctx, u) c.Assert(err, gc.Equals, nil) _, err = s.JIMM.UpdateCloudCredential(ctx, user, jimm.UpdateCloudCredentialArgs{ CredentialTag: tag, @@ -251,13 +250,13 @@ func (s *JimmCmdSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialT func (s *JimmCmdSuite) AddModel(c *gc.C, owner names.UserTag, name string, cloud names.CloudTag, region string, cred names.CloudCredentialTag) names.ModelTag { ctx := context.Background() + i, err := dbmodel.NewIdentity(owner.Id()) + c.Assert(err, gc.IsNil) u := openfga.NewUser( - &dbmodel.Identity{ - Name: owner.Id(), - }, + i, s.OFGAClient, ) - err := s.JIMM.Database.GetIdentity(ctx, u.Identity) + err = s.JIMM.Database.GetIdentity(ctx, u.Identity) c.Assert(err, gc.Equals, nil) mi, err := s.JIMM.AddModel(ctx, u, &jimm.ModelCreateArgs{ Name: name, diff --git a/internal/db/applicationoffer_test.go b/internal/db/applicationoffer_test.go index 6d7a38d48..69ce00211 100644 --- a/internal/db/applicationoffer_test.go +++ b/internal/db/applicationoffer_test.go @@ -39,10 +39,10 @@ func initTestEnvironment(c *qt.C, db *db.Database) testEnvironment { c.Assert(err, qt.Equals, nil) env := testEnvironment{} + i, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + env.u = *i - env.u = dbmodel.Identity{ - Name: "bob@canonical.com", - } c.Assert(db.DB.Create(&env.u).Error, qt.IsNil) env.cloud = dbmodel.Cloud{ @@ -241,9 +241,8 @@ func (s *dbSuite) TestFindApplicationOffers(c *qt.C) { err := s.Database.AddApplicationOffer(context.Background(), &offer1) c.Assert(err, qt.Equals, nil) - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) offer2 := dbmodel.ApplicationOffer{ diff --git a/internal/db/cloudcredential_test.go b/internal/db/cloudcredential_test.go index 837c357b7..fdd3e62bd 100644 --- a/internal/db/cloudcredential_test.go +++ b/internal/db/cloudcredential_test.go @@ -30,9 +30,8 @@ func (s *dbSuite) TestSetCloudCredentialInvalidTag(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) cloud := dbmodel.Cloud{ @@ -58,9 +57,8 @@ func (s *dbSuite) TestSetCloudCredential(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) cloud := dbmodel.Cloud{ @@ -95,9 +93,8 @@ func (s *dbSuite) TestSetCloudCredentialUpdate(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) cloud := dbmodel.Cloud{ @@ -165,9 +162,8 @@ func (s *dbSuite) TestGetCloudCredential(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) cloud := dbmodel.Cloud{ diff --git a/internal/db/clouddefaults_test.go b/internal/db/clouddefaults_test.go index 408dfc11e..bad170d71 100644 --- a/internal/db/clouddefaults_test.go +++ b/internal/db/clouddefaults_test.go @@ -22,9 +22,8 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { err := s.Database.Migrate(ctx, true) c.Assert(err, qt.Equals, nil) - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) cloud1 := dbmodel.Cloud{ @@ -49,7 +48,7 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { cloud.Regions = nil defaults := dbmodel.CloudDefaults{ IdentityName: u.Name, - Identity: u, + Identity: *u, CloudID: cloud.ID, Cloud: cloud, Region: cloud1.Regions[0].Name, @@ -61,7 +60,7 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { err = s.Database.SetCloudDefaults(ctx, &defaults) c.Check(err, qt.Equals, nil) - d, err := s.Database.ModelDefaultsForCloud(ctx, &u, names.NewCloudTag("test-cloud-1")) + d, err := s.Database.ModelDefaultsForCloud(ctx, u, names.NewCloudTag("test-cloud-1")) c.Assert(err, qt.Equals, nil) c.Assert(d, qt.HasLen, 1) c.Assert(d[0], qt.DeepEquals, defaults) @@ -71,7 +70,7 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { err = s.Database.SetCloudDefaults(ctx, &defaults) c.Check(err, qt.Equals, nil) - d, err = s.Database.ModelDefaultsForCloud(ctx, &u, names.NewCloudTag("test-cloud-1")) + d, err = s.Database.ModelDefaultsForCloud(ctx, u, names.NewCloudTag("test-cloud-1")) c.Assert(err, qt.Equals, nil) c.Assert(d, qt.HasLen, 1) c.Assert(d[0].Defaults, qt.DeepEquals, dbmodel.Map{ @@ -107,7 +106,7 @@ func (s *dbSuite) TestModelDefaults(c *qt.C) { c.Assert(err, qt.Equals, nil) c.Assert(dbDefaults, qt.CmpEquals(cmpopts.IgnoreTypes([]dbmodel.CloudRegion{}, gorm.Model{})), dbmodel.CloudDefaults{ IdentityName: u.Name, - Identity: u, + Identity: *u, CloudID: cloud1.ID, Cloud: cloud1, Region: cloud1.Regions[0].Name, @@ -155,7 +154,9 @@ func TestModelDefaultsForCloudUnconfiguredDatabase(t *testing.T) { c := qt.New(t) var d db.Database - _, err := d.ModelDefaultsForCloud(context.Background(), &dbmodel.Identity{}, names.NewCloudTag("test-cloud")) + i, err := dbmodel.NewIdentity("bob") + c.Assert(err, qt.IsNil) + _, err = d.ModelDefaultsForCloud(context.Background(), i, names.NewCloudTag("test-cloud")) c.Check(err, qt.ErrorMatches, `database not configured`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeServerConfiguration) } diff --git a/internal/db/controller_test.go b/internal/db/controller_test.go index 43120d0bd..ca0404808 100644 --- a/internal/db/controller_test.go +++ b/internal/db/controller_test.go @@ -122,15 +122,14 @@ func (s *dbSuite) TestGetControllerWithModels(c *qt.C) { CloudName: "test-cloud", CloudRegion: "test-region", } - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) cred := dbmodel.CloudCredential{ Name: "test-cred", Cloud: cloud, - Owner: u, + Owner: *u, AuthType: "empty", } c.Assert(s.Database.DB.Create(&cred).Error, qt.IsNil) @@ -144,7 +143,7 @@ func (s *dbSuite) TestGetControllerWithModels(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - Owner: u, + Owner: *u, Controller: controller, CloudRegion: cloud.Regions[0], CloudCredential: cred, @@ -168,7 +167,7 @@ func (s *dbSuite) TestGetControllerWithModels(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000002", Valid: true, }, - Owner: u, + Owner: *u, Controller: controller, CloudRegion: cloud.Regions[0], CloudCredential: cred, diff --git a/internal/db/db_test.go b/internal/db/db_test.go index b89db0613..0880fbbb2 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -59,10 +59,11 @@ func (s *dbSuite) TestTransaction(c *qt.C) { err = s.Database.Migrate(context.Background(), false) c.Assert(err, qt.IsNil) - + i, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) err = s.Database.Transaction(func(d *db.Database) error { c.Check(d, qt.Not(qt.Equals), s.Database) - return d.GetIdentity(context.Background(), &dbmodel.Identity{Name: "bob@canonical.com"}) + return d.GetIdentity(context.Background(), i) }) c.Assert(err, qt.IsNil) diff --git a/internal/db/identity_test.go b/internal/db/identity_test.go index cfbcbf192..d3816f766 100644 --- a/internal/db/identity_test.go +++ b/internal/db/identity_test.go @@ -17,82 +17,114 @@ func TestGetIdentityUnconfiguredDatabase(t *testing.T) { c := qt.New(t) var d db.Database - err := d.GetIdentity(context.Background(), &dbmodel.Identity{}) + i, err := dbmodel.NewIdentity("bob") + c.Assert(err, qt.IsNil) + err = d.GetIdentity(context.Background(), i) c.Check(err, qt.ErrorMatches, `database not configured`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeServerConfiguration) } func (s *dbSuite) TestGetIdentity(c *qt.C) { ctx := context.Background() - err := s.Database.GetIdentity(ctx, &dbmodel.Identity{}) + i, err := dbmodel.NewIdentity("bob") + c.Assert(err, qt.IsNil) + err = s.Database.GetIdentity(ctx, i) c.Check(err, qt.ErrorMatches, `upgrade in progress`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeUpgradeInProgress) err = s.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - err = s.Database.GetIdentity(ctx, &dbmodel.Identity{}) - c.Check(err, qt.ErrorMatches, `invalid identity name ""`) - c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) - - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } - err = s.Database.GetIdentity(ctx, &u) + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + err = s.Database.GetIdentity(ctx, u) c.Assert(err, qt.IsNil) - u2 := dbmodel.Identity{ - Name: u.Name, - } - err = s.Database.GetIdentity(ctx, &u2) + u2, err := dbmodel.NewIdentity(u.Name) + c.Assert(err, qt.IsNil) + err = s.Database.GetIdentity(ctx, u2) c.Assert(err, qt.IsNil) c.Check(u2, qt.DeepEquals, u) + + u3, err := dbmodel.NewIdentity("jimm_test@canonical.com") + c.Assert(err, qt.IsNil) + err = s.Database.GetIdentity(ctx, u3) + c.Assert(err, qt.IsNil) + c.Check(u3.Name, qt.DeepEquals, "jimm-test43cc8c@canonical.com") + + // Test get on the sanitised email returns ONLY the sanitised user + // and doesn't create a new user + u4, err := dbmodel.NewIdentity("jimm-test43cc8c@canonical.com") + c.Assert(err, qt.IsNil) + err = s.Database.GetIdentity(ctx, u4) + c.Assert(err, qt.IsNil) + c.Check(u4, qt.DeepEquals, u3) } func TestUpdateIdentityUnconfiguredDatabase(t *testing.T) { c := qt.New(t) var d db.Database - err := d.UpdateIdentity(context.Background(), &dbmodel.Identity{}) + + i, err := dbmodel.NewIdentity("bob") + c.Assert(err, qt.IsNil) + err = d.UpdateIdentity(context.Background(), i) c.Check(err, qt.ErrorMatches, `database not configured`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeServerConfiguration) } func (s *dbSuite) TestUpdateIdentity(c *qt.C) { ctx := context.Background() - err := s.Database.UpdateIdentity(ctx, &dbmodel.Identity{}) + + i, err := dbmodel.NewIdentity("bob") + c.Assert(err, qt.IsNil) + err = s.Database.UpdateIdentity(ctx, i) c.Check(err, qt.ErrorMatches, `upgrade in progress`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeUpgradeInProgress) err = s.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - err = s.Database.UpdateIdentity(ctx, &dbmodel.Identity{}) - c.Check(err, qt.ErrorMatches, `invalid identity name ""`) - c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) - - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } - err = s.Database.GetIdentity(ctx, &u) + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + err = s.Database.GetIdentity(ctx, u) c.Assert(err, qt.IsNil) - err = s.Database.UpdateIdentity(ctx, &u) + err = s.Database.UpdateIdentity(ctx, u) c.Assert(err, qt.IsNil) - u2 := dbmodel.Identity{ - Name: u.Name, - } - err = s.Database.GetIdentity(ctx, &u2) + u2, err := dbmodel.NewIdentity(u.Name) + c.Assert(err, qt.IsNil) + err = s.Database.GetIdentity(ctx, u2) c.Assert(err, qt.IsNil) c.Check(u2, qt.DeepEquals, u) + + u3, err := dbmodel.NewIdentity("jimm_test@canonical.com") + c.Assert(err, qt.IsNil) + err = s.Database.GetIdentity(ctx, u3) + c.Assert(err, qt.IsNil) + c.Check(u3.Name, qt.DeepEquals, "jimm-test43cc8c@canonical.com") + + u3.AccessToken = "REMOVED-ACCESS-TOKEN-EXAMPLE" + err = s.Database.UpdateIdentity(ctx, u3) + c.Assert(err, qt.IsNil) + + // Do a final get just to be super clear the updates have taken effect on the + // sanitised user + u4, err := dbmodel.NewIdentity(u3.Name) + c.Assert(err, qt.IsNil) + err = s.Database.GetIdentity(ctx, u4) + c.Assert(err, qt.IsNil) + c.Assert(u4, qt.DeepEquals, u3) } func TestGetIdentityCloudCredentialsUnconfiguredDatabase(t *testing.T) { c := qt.New(t) var d db.Database - _, err := d.GetIdentityCloudCredentials(context.Background(), &dbmodel.Identity{}, "") + i, err := dbmodel.NewIdentity("bob") + c.Assert(err, qt.IsNil) + _, err = d.GetIdentityCloudCredentials(context.Background(), i, "") c.Check(err, qt.ErrorMatches, `database not configured`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeServerConfiguration) } @@ -103,19 +135,19 @@ func (s *dbSuite) TestGetIdentityCloudCredentials(c *qt.C) { err := s.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - _, err = s.Database.GetIdentityCloudCredentials(ctx, &dbmodel.Identity{}, "") + i, err := dbmodel.NewIdentity("idontexist") + c.Assert(err, qt.IsNil) + _, err = s.Database.GetIdentityCloudCredentials(ctx, i, "") c.Check(err, qt.ErrorMatches, `cloudcredential not found`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) - _, err = s.Database.GetIdentityCloudCredentials(ctx, &dbmodel.Identity{ - Name: "test", - }, "ec2") + i, err = dbmodel.NewIdentity("test") + _, err = s.Database.GetIdentityCloudCredentials(ctx, i, "ec2") c.Check(err, qt.IsNil) - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) + i, err = dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(s.Database.DB.Create(i).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -129,7 +161,7 @@ func (s *dbSuite) TestGetIdentityCloudCredentials(c *qt.C) { cred1 := dbmodel.CloudCredential{ Name: "test-cred-1", CloudName: cloud.Name, - OwnerIdentityName: u.Name, + OwnerIdentityName: i.Name, AuthType: "empty", } err = s.Database.SetCloudCredential(context.Background(), &cred1) @@ -138,13 +170,13 @@ func (s *dbSuite) TestGetIdentityCloudCredentials(c *qt.C) { cred2 := dbmodel.CloudCredential{ Name: "test-cred-2", CloudName: cloud.Name, - OwnerIdentityName: u.Name, + OwnerIdentityName: i.Name, AuthType: "empty", } err = s.Database.SetCloudCredential(context.Background(), &cred2) c.Assert(err, qt.Equals, nil) - credentials, err := s.Database.GetIdentityCloudCredentials(ctx, &u, cloud.Name) + credentials, err := s.Database.GetIdentityCloudCredentials(ctx, i, cloud.Name) c.Check(err, qt.IsNil) c.Assert(credentials, qt.DeepEquals, []dbmodel.CloudCredential{cred1, cred2}) } diff --git a/internal/db/identitymodeldefaults_test.go b/internal/db/identitymodeldefaults_test.go index a3922f14a..7f661e55d 100644 --- a/internal/db/identitymodeldefaults_test.go +++ b/internal/db/identitymodeldefaults_test.go @@ -37,9 +37,9 @@ func TestSetIdentityModelDefaults(t *testing.T) { }{{ about: "defaults do not exist yet - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - identity := dbmodel.Identity{ - Name: "bob@canonical.com", - } + i, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + identity := i c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) defaults := map[string]interface{}{ @@ -49,12 +49,12 @@ func TestSetIdentityModelDefaults(t *testing.T) { expectedDefaults := dbmodel.IdentityModelDefaults{ IdentityName: identity.Name, - Identity: identity, + Identity: *i, Defaults: defaults, } return testConfig{ - identity: &identity, + identity: i, defaults: defaults, expectedDefaults: &expectedDefaults, } @@ -62,14 +62,13 @@ func TestSetIdentityModelDefaults(t *testing.T) { }, { about: "defaults already exist - defaults updated", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - identity := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) + i, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(i).Error, qt.IsNil) j.Database.SetIdentityModelDefaults(ctx, &dbmodel.IdentityModelDefaults{ - IdentityName: identity.Name, - Identity: identity, + IdentityName: i.Name, + Identity: *i, Defaults: map[string]interface{}{ "key1": float64(17), "key2": "a test string", @@ -83,13 +82,13 @@ func TestSetIdentityModelDefaults(t *testing.T) { } expectedDefaults := dbmodel.IdentityModelDefaults{ - IdentityName: identity.Name, - Identity: identity, + IdentityName: i.Name, + Identity: *i, Defaults: defaults, } return testConfig{ - identity: &identity, + identity: i, defaults: defaults, expectedDefaults: &expectedDefaults, } @@ -97,9 +96,8 @@ func TestSetIdentityModelDefaults(t *testing.T) { }, { about: "identity does not exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - identity := dbmodel.Identity{ - Name: "bob@canonical.com", - } + i, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) defaults := map[string]interface{}{ "key1": float64(42), @@ -108,7 +106,7 @@ func TestSetIdentityModelDefaults(t *testing.T) { } return testConfig{ - identity: &identity, + identity: i, defaults: defaults, expectedError: `.*violates foreign key constraint.*`, } @@ -116,10 +114,9 @@ func TestSetIdentityModelDefaults(t *testing.T) { }, { about: "cannot set agent-version", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - identity := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) + i, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(i).Error, qt.IsNil) defaults := map[string]interface{}{ "agent-version": "2.0", @@ -128,7 +125,7 @@ func TestSetIdentityModelDefaults(t *testing.T) { } return testConfig{ - identity: &identity, + identity: i, defaults: defaults, expectedError: `agent-version cannot have a default value`, } diff --git a/internal/db/model.go b/internal/db/model.go index 69c4e58e5..c4e1c6448 100644 --- a/internal/db/model.go +++ b/internal/db/model.go @@ -173,6 +173,9 @@ func preloadModel(prefix string, db *gorm.DB) *gorm.DB { db = db.Preload(prefix + "Owner") db = db.Preload(prefix + "Controller") db = db.Preload(prefix + "CloudRegion").Preload(prefix + "CloudRegion.Cloud") + // We don't care about the cloud credential owner when + // loading a model, as we just use the credential to deploy + // applications. db = db.Preload(prefix + "CloudCredential") db = db.Preload(prefix + "Offers").Preload(prefix + "Offers.Connections").Preload(prefix + "Offers.Endpoints").Preload(prefix + "Offers.Spaces") diff --git a/internal/db/model_test.go b/internal/db/model_test.go index 97105f555..19a6db204 100644 --- a/internal/db/model_test.go +++ b/internal/db/model_test.go @@ -31,9 +31,8 @@ func (s *dbSuite) TestAddModel(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) cloud := dbmodel.Cloud{ @@ -48,7 +47,7 @@ func (s *dbSuite) TestAddModel(c *qt.C) { cred := dbmodel.CloudCredential{ Name: "test-cred", Cloud: cloud, - Owner: u, + Owner: *u, AuthType: "empty", } c.Assert(s.Database.DB.Create(&cred).Error, qt.IsNil) @@ -104,10 +103,9 @@ func (s *dbSuite) TestGetModel(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(s.Database.DB.Create(u).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -121,7 +119,7 @@ func (s *dbSuite) TestGetModel(c *qt.C) { cred := dbmodel.CloudCredential{ Name: "test-cred", Cloud: cloud, - Owner: u, + Owner: *u, AuthType: "empty", } c.Assert(s.Database.DB.Create(&cred).Error, qt.IsNil) @@ -143,7 +141,7 @@ func (s *dbSuite) TestGetModel(c *qt.C) { Valid: true, }, OwnerIdentityName: u.Name, - Owner: u, + Owner: *u, ControllerID: controller.ID, Controller: controller, CloudRegionID: cloud.Regions[0].ID, @@ -162,6 +160,9 @@ func (s *dbSuite) TestGetModel(c *qt.C) { }, } model.CloudCredential.Cloud = dbmodel.Cloud{} + // We don't care about the cloud credential owner when + // loading a model, as we just use the credential to deploy + // applications. Hence, we don't use NewIdentity() here model.CloudCredential.Owner = dbmodel.Identity{} err = s.Database.AddModel(context.Background(), &model) c.Assert(err, qt.Equals, nil) @@ -204,10 +205,9 @@ func (s *dbSuite) TestUpdateModel(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) + i, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(s.Database.DB.Create(i).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -221,7 +221,7 @@ func (s *dbSuite) TestUpdateModel(c *qt.C) { cred := dbmodel.CloudCredential{ Name: "test-cred", Cloud: cloud, - Owner: u, + Owner: *i, AuthType: "empty", } c.Assert(s.Database.DB.Create(&cred).Error, qt.IsNil) @@ -238,7 +238,7 @@ func (s *dbSuite) TestUpdateModel(c *qt.C) { model := dbmodel.Model{ Name: "test-model-1", - OwnerIdentityName: u.Name, + OwnerIdentityName: i.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred.ID, @@ -282,10 +282,9 @@ func (s *dbSuite) TestDeleteModel(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) + i, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(s.Database.DB.Create(i).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -299,7 +298,7 @@ func (s *dbSuite) TestDeleteModel(c *qt.C) { cred := dbmodel.CloudCredential{ Name: "test-cred", Cloud: cloud, - Owner: u, + Owner: *i, AuthType: "empty", } c.Assert(s.Database.DB.Create(&cred).Error, qt.IsNil) @@ -316,7 +315,7 @@ func (s *dbSuite) TestDeleteModel(c *qt.C) { model := dbmodel.Model{ Name: "test-model-1", - OwnerIdentityName: u.Name, + OwnerIdentityName: i.Name, ControllerID: controller.ID, Controller: controller, CloudRegionID: cloud.Regions[0].ID, @@ -356,10 +355,9 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { err := s.Database.Migrate(context.Background(), true) c.Assert(err, qt.Equals, nil) - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(s.Database.DB.Create(&u).Error, qt.IsNil) + i, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(s.Database.DB.Create(i).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -373,7 +371,7 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { cred1 := dbmodel.CloudCredential{ Name: "test-cred-1", Cloud: cloud, - Owner: u, + Owner: *i, AuthType: "empty", } c.Assert(s.Database.DB.Create(&cred1).Error, qt.IsNil) @@ -381,7 +379,7 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { cred2 := dbmodel.CloudCredential{ Name: "test-cred-2", Cloud: cloud, - Owner: u, + Owner: *i, AuthType: "empty", } c.Assert(s.Database.DB.Create(&cred2).Error, qt.IsNil) @@ -401,7 +399,7 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - OwnerIdentityName: u.Name, + OwnerIdentityName: i.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred1.ID, @@ -425,7 +423,7 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000002", Valid: true, }, - OwnerIdentityName: u.Name, + OwnerIdentityName: i.Name, ControllerID: controller.ID, CloudRegionID: cloud.Regions[0].ID, CloudCredentialID: cred2.ID, @@ -454,7 +452,7 @@ func (s *dbSuite) TestGetModelsUsingCredential(c *qt.C) { String: "00000001-0000-0000-0000-0000-000000000001", Valid: true, }, - OwnerIdentityName: u.Name, + OwnerIdentityName: i.Name, ControllerID: controller.ID, Controller: controller, CloudRegionID: cloud.Regions[0].ID, diff --git a/internal/dbmodel/cloudcredential_test.go b/internal/dbmodel/cloudcredential_test.go index 73f2f7ee6..d79656fdd 100644 --- a/internal/dbmodel/cloudcredential_test.go +++ b/internal/dbmodel/cloudcredential_test.go @@ -30,15 +30,14 @@ func TestCloudCredentialTag(t *testing.T) { func TestCloudCredential(t *testing.T) { c := qt.New(t) db := gormDB(c) - + i, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ Name: "test-credential", Cloud: dbmodel.Cloud{ Name: "test-cloud", }, - Owner: dbmodel.Identity{ - Name: "bob@canonical.com", - }, + Owner: *i, AuthType: "empty", Label: "test label", Attributes: dbmodel.StringMap{ @@ -65,13 +64,12 @@ func TestCloudCredentialsCascadeOnDelete(t *testing.T) { result := db.Create(&cloud) c.Assert(result.Error, qt.IsNil) c.Check(result.RowsAffected, qt.Equals, int64(1)) - + i, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) cred := dbmodel.CloudCredential{ Name: "test-credential", Cloud: cloud, - Owner: dbmodel.Identity{ - Name: "bob@canonical.com", - }, + Owner: *i, } result = db.Create(&cred) c.Assert(result.Error, qt.IsNil) diff --git a/internal/dbmodel/controller_test.go b/internal/dbmodel/controller_test.go index 2ac7b7d75..d2b540fbb 100644 --- a/internal/dbmodel/controller_test.go +++ b/internal/dbmodel/controller_test.go @@ -90,10 +90,9 @@ func TestControllerModels(t *testing.T) { CloudCredential: cred, } c.Assert(db.Create(&m1).Error, qt.IsNil) + u2, err := dbmodel.NewIdentity("charlie@canonical.com") + c.Assert(err, qt.IsNil) - u2 := dbmodel.Identity{ - Name: "charlie@canonical.com", - } c.Assert(db.Create(&u2).Error, qt.IsNil) m2 := dbmodel.Model{ @@ -102,7 +101,7 @@ func TestControllerModels(t *testing.T) { String: "00000001-0000-0000-0000-0000-000000000002", Valid: true, }, - Owner: u2, + Owner: *u2, Controller: ctl, CloudRegion: cl.Regions[0], CloudCredential: cred, @@ -110,7 +109,7 @@ func TestControllerModels(t *testing.T) { c.Assert(db.Create(&m2).Error, qt.IsNil) var models []dbmodel.Model - err := db.Model(&ctl).Association("Models").Find(&models) + err = db.Model(&ctl).Association("Models").Find(&models) c.Assert(err, qt.IsNil) c.Check(models, qt.DeepEquals, []dbmodel.Model{{ diff --git a/internal/dbmodel/identity.go b/internal/dbmodel/identity.go index 1888af930..a5f83de31 100644 --- a/internal/dbmodel/identity.go +++ b/internal/dbmodel/identity.go @@ -3,7 +3,11 @@ package dbmodel import ( + "crypto/sha256" "database/sql" + "errors" + "fmt" + "strings" "time" jujuparams "github.com/juju/juju/rpc/params" @@ -11,6 +15,25 @@ import ( "gorm.io/gorm" ) +var ( + // IdentityCreationError holds the error to be returned on failures to create + // an identity model. + IdentityCreationError = errors.New("identity name cannot be empty") +) + +// NewIdentity returns an Identity with the Name and DisplayName fields set. +func NewIdentity(name string) (*Identity, error) { + if name == "" { + return nil, IdentityCreationError + } + i := &Identity{ + Name: name, + } + i.santiseIdentityId() + i.setDisplayName() + return i, nil +} + // Identity represents a JIMM identity, which can be a user or a service account. type Identity struct { gorm.Model @@ -83,3 +106,47 @@ func (i Identity) ToJujuUserInfo() jujuparams.UserInfo { } return ui } + +// SanitiseIdentityId ensures that the identity id persisted is safe +// for use in Juju tags, this is done by replacing all of the unsafe +// email characters AND underscores (despite being safe in emails) with +// hyphens. See the corresponding test for examples of sanitisations. +func (i *Identity) santiseIdentityId() { + userTagReplacer := strings.NewReplacer( + "~", "-", + "!", "-", + "$", "-", + "%", "-", + "^", "-", + "&", "-", + "*", "-", + "_", "-", + "=", "-", + "{", "-", + "}", "-", + "'", "-", + "?", "-", + ) + + replaced := userTagReplacer.Replace(i.Name) + + if replaced == i.Name { + return + } + + hash := sha256.Sum256([]byte(i.Name)) + shortHash := fmt.Sprintf("%x", hash[:3]) + replacedWithSHA := strings.Replace(replaced, "@", shortHash+"@", 1) + i.Name = replacedWithSHA +} + +// SetDisplayName ensures that DisplayNames are set to the first part of +// an email (example@domain.com -> example) or client id (uuid@serviceaccount -> uuid) +// for use within the dashboard. +// +// Note: It will only set the display name if the display name is NOT set. +func (i *Identity) setDisplayName() { + if i.DisplayName == "" { + i.DisplayName = strings.Split(i.Name, "@")[0] + } +} diff --git a/internal/dbmodel/identity_test.go b/internal/dbmodel/identity_test.go index 29828e100..8c0dd17c6 100644 --- a/internal/dbmodel/identity_test.go +++ b/internal/dbmodel/identity_test.go @@ -19,19 +19,19 @@ func TestIdentity(t *testing.T) { c := qt.New(t) db := gormDB(c) - var u0 dbmodel.Identity + u0, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) result := db.Where("name = ?", "bob@canonical.com").First(&u0) c.Check(result.Error, qt.Equals, gorm.ErrRecordNotFound) - u1 := dbmodel.Identity{ - Name: "bob@canonical.com", - DisplayName: "bob", - } - result = db.Create(&u1) + u1, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + result = db.Create(u1) c.Assert(result.Error, qt.IsNil) c.Check(result.RowsAffected, qt.Equals, int64(1)) - var u2 dbmodel.Identity + u2, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) result = db.Where("name = ?", "bob@canonical.com").First(&u2) c.Assert(result.Error, qt.IsNil) c.Check(u2, qt.DeepEquals, u1) @@ -40,15 +40,14 @@ func TestIdentity(t *testing.T) { u2.LastLogin.Valid = true result = db.Save(&u2) c.Assert(result.Error, qt.IsNil) - var u3 dbmodel.Identity + u3, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) result = db.Where("name = ?", "bob@canonical.com").First(&u3) c.Assert(result.Error, qt.IsNil) c.Check(u3, qt.DeepEquals, u2) - u4 := dbmodel.Identity{ - Name: "bob@canonical.com", - DisplayName: "bob", - } + u4, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) result = db.Create(&u4) c.Check(result.Error, qt.ErrorMatches, `.*violates unique constraint "identities_name_key".*`) } @@ -56,12 +55,12 @@ func TestIdentity(t *testing.T) { func TestUserTag(t *testing.T) { c := qt.New(t) - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) tag := u.Tag() c.Check(tag.String(), qt.Equals, "user-bob@canonical.com") - var u2 dbmodel.Identity + u2, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) u2.SetTag(tag.(names.UserTag)) c.Check(u2, qt.DeepEquals, u) } @@ -76,16 +75,15 @@ func TestIdentityCloudCredentials(t *testing.T) { result := db.Create(&cl) c.Assert(result.Error, qt.IsNil) - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) result = db.Create(&u) c.Assert(result.Error, qt.IsNil) cred1 := dbmodel.CloudCredential{ Name: "test-cred-1", Cloud: cl, - Owner: u, + Owner: *u, AuthType: "empty", } result = db.Create(&cred1) @@ -94,14 +92,14 @@ func TestIdentityCloudCredentials(t *testing.T) { cred2 := dbmodel.CloudCredential{ Name: "test-cred-2", Cloud: cl, - Owner: u, + Owner: *u, AuthType: "empty", } result = db.Create(&cred2) c.Assert(result.Error, qt.IsNil) var creds []dbmodel.CloudCredential - err := db.Model(u).Association("CloudCredentials").Find(&creds) + err = db.Model(u).Association("CloudCredentials").Find(&creds) c.Assert(err, qt.IsNil) c.Check(creds, qt.DeepEquals, []dbmodel.CloudCredential{{ Model: cred1.Model, @@ -121,17 +119,16 @@ func TestIdentityCloudCredentials(t *testing.T) { func TestIdentityToJujuUserInfo(t *testing.T) { c := qt.New(t) - u := dbmodel.Identity{ - Model: gorm.Model{ - CreatedAt: time.Now(), - }, - Name: "alice@canonical.com", - DisplayName: "Alice", + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + u.Model = gorm.Model{ + CreatedAt: time.Now(), } + ui := u.ToJujuUserInfo() c.Check(ui, qt.DeepEquals, jujuparams.UserInfo{ Username: "alice@canonical.com", - DisplayName: "Alice", + DisplayName: "alice", Access: "", DateCreated: u.CreatedAt, }) @@ -143,9 +140,60 @@ func TestIdentityToJujuUserInfo(t *testing.T) { ui = u.ToJujuUserInfo() c.Check(ui, qt.DeepEquals, jujuparams.UserInfo{ Username: "alice@canonical.com", - DisplayName: "Alice", + DisplayName: "alice", Access: "", DateCreated: u.CreatedAt, LastConnection: &u.LastLogin.Time, }) } + +func TestNewIdentity(t *testing.T) { + c := qt.New(t) + + tests := []struct { + about string + input string + expectedSanitisedEmailOrClientId string + expectedDisplayName string + }{ + { + about: "catch all test", + input: "hi~!$%^&*_=}{'?@~!$%^&*_=}{'?bye.com", + expectedSanitisedEmailOrClientId: "hi-------------861fcb@-------------bye.com", + expectedDisplayName: "hi-------------861fcb", + }, + { + about: "test bad email", + input: "alice_wonderland@bad_domain.com", + expectedSanitisedEmailOrClientId: "alice-wonderland39cfd5@bad-domain.com", + expectedDisplayName: "alice-wonderland39cfd5", + }, + { + about: "test good email", + input: "alice-wonderland@good-domain.com", + expectedSanitisedEmailOrClientId: "alice-wonderland@good-domain.com", + expectedDisplayName: "alice-wonderland", + }, + { + about: "test good service account", + input: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", + expectedSanitisedEmailOrClientId: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", + expectedDisplayName: "fca1f605-736e-4d1f-bcd2-aecc726923be", + }, + { + about: "test bad service account", + input: "fca1f605_736e_4d1f_bcd2_aecc726923be@serviceaccount", + expectedSanitisedEmailOrClientId: "fca1f605-736e-4d1f-bcd2-aecc726923be28d4eb@serviceaccount", + expectedDisplayName: "fca1f605-736e-4d1f-bcd2-aecc726923be28d4eb", + }, + } + for _, tc := range tests { + c.Run(tc.about, func(c *qt.C) { + i, err := dbmodel.NewIdentity(tc.input) + c.Assert(err, qt.IsNil) + + c.Assert(i.Name, qt.Equals, tc.expectedSanitisedEmailOrClientId) + c.Assert(i.DisplayName, qt.Equals, tc.expectedDisplayName) + }) + } +} diff --git a/internal/dbmodel/model.go b/internal/dbmodel/model.go index 74567d3d9..e589fae15 100644 --- a/internal/dbmodel/model.go +++ b/internal/dbmodel/model.go @@ -47,7 +47,7 @@ type Model struct { // CloudCredential is the credential used with the model. CloudCredentialID uint - CloudCredential CloudCredential + CloudCredential CloudCredential `gorm:"foreignkey:CloudCredentialID;references:ID"` // Type is the type of model. Type string @@ -108,7 +108,10 @@ func (m *Model) SwitchOwner(u *Identity) { m.Owner = *u } -// FromJujuModelInfo converts jujuparams.ModelInfo into Model. +// FromJujuModelInfo converts on a best-effort basis jujuparams.ModelInfo into Model. +// +// Some fields specific to JIMM which aren't present in a jujuparams.ModelInfo type +// will need to be filled in manually by the caller of this function. func (m *Model) FromJujuModelInfo(info jujuparams.ModelInfo) error { m.Name = info.Name m.Type = info.Type diff --git a/internal/dbmodel/model_test.go b/internal/dbmodel/model_test.go index c573c92a2..411f7ce0e 100644 --- a/internal/dbmodel/model_test.go +++ b/internal/dbmodel/model_test.go @@ -317,9 +317,9 @@ func TestToJujuModelSummary(t *testing.T) { // initModelEnv initialises a controller, cloud and cloud-credential so // that a model can be created. func initModelEnv(c *qt.C, db *gorm.DB) (dbmodel.Cloud, dbmodel.CloudCredential, dbmodel.Controller, dbmodel.Identity) { - u := dbmodel.Identity{ - Name: "bob@canonical.com", - } + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.Create(&u).Error, qt.IsNil) cl := dbmodel.Cloud{ @@ -334,7 +334,7 @@ func initModelEnv(c *qt.C, db *gorm.DB) (dbmodel.Cloud, dbmodel.CloudCredential, cred := dbmodel.CloudCredential{ Name: "test-cred", Cloud: cl, - Owner: u, + Owner: *u, AuthType: "empty", } c.Assert(db.Create(&cred).Error, qt.IsNil) @@ -347,7 +347,7 @@ func initModelEnv(c *qt.C, db *gorm.DB) (dbmodel.Cloud, dbmodel.CloudCredential, } c.Assert(db.Create(&ctl).Error, qt.IsNil) - return cl, cred, ctl, u + return cl, cred, ctl, *u } func TestModelFromJujuModelInfo(t *testing.T) { @@ -376,7 +376,7 @@ func TestModelFromJujuModelInfo(t *testing.T) { }, Users: []jujuparams.ModelUserInfo{{ UserName: "bob@canonical.com", - DisplayName: "Bobby The Tester", + DisplayName: "bob", Access: "admin", }}, Machines: []jujuparams.ModelMachineInfo{{ @@ -399,6 +399,14 @@ func TestModelFromJujuModelInfo(t *testing.T) { err := model.FromJujuModelInfo(modelInfo) c.Assert(err, qt.IsNil) + i, err := dbmodel.NewIdentity("bob@canonical.com") + // We set display name to nothing, as when running from model info + // you will never get a display name. The way we use FromJujuModelInfo is that + // we get as much as we can from the model info, and fill in the bits of + // the dbmodel.Model (like the identity) where we can. As such, this doesn't + // need to be tested and doesn't make any sense. + i.DisplayName = "" + c.Assert(err, qt.IsNil) c.Assert(model, qt.DeepEquals, dbmodel.Model{ Name: "test-model", UUID: sql.NullString{ @@ -414,9 +422,7 @@ func TestModelFromJujuModelInfo(t *testing.T) { CloudCredential: dbmodel.CloudCredential{ Name: "test-cred", CloudName: "test-cloud", - Owner: dbmodel.Identity{ - Name: "bob@canonical.com", - }, + Owner: *i, }, OwnerIdentityName: "bob@canonical.com", Type: "iaas", diff --git a/internal/discharger/discharger.go b/internal/discharger/discharger.go index a30b3a813..80fe46f52 100644 --- a/internal/discharger/discharger.go +++ b/internal/discharger/discharger.go @@ -122,10 +122,12 @@ func (md *MacaroonDischarger) CheckThirdPartyCaveat(ctx context.Context, req *ht offerTag := jimmnames.NewApplicationOfferTag(offerUUID) + i, err := dbmodel.NewIdentity(userTag.Id()) + if err != nil { + return nil, err + } user := openfga.NewUser( - &dbmodel.Identity{ - Name: userTag.Id(), - }, + i, md.ofgaClient, ) diff --git a/internal/jimm/access_test.go b/internal/jimm/access_test.go index 24ac7b061..369eba708 100644 --- a/internal/jimm/access_test.go +++ b/internal/jimm/access_test.go @@ -41,10 +41,12 @@ func (ta *testAuthenticator) Authenticate(ctx context.Context, req *jujuparams.L if ta.err != nil { return nil, ta.err } + i, err := dbmodel.NewIdentity(ta.username) + if err != nil { + return nil, err + } return &openfga.User{ - Identity: &dbmodel.Identity{ - Name: ta.username, - }, + Identity: i, }, nil } @@ -149,12 +151,15 @@ func TestAuditLogAccess(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - - adminUser := openfga.NewUser(&dbmodel.Identity{Name: "alice"}, j.OpenFGAClient) + i, err := dbmodel.NewIdentity("alice") + c.Assert(err, qt.IsNil) + adminUser := openfga.NewUser(i, j.OpenFGAClient) err = adminUser.SetControllerAccess(ctx, j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) - user := openfga.NewUser(&dbmodel.Identity{Name: "bob"}, j.OpenFGAClient) + i2, err := dbmodel.NewIdentity("bob") + c.Assert(err, qt.IsNil) + user := openfga.NewUser(i2, j.OpenFGAClient) // admin user can grant other users audit log access. err = j.GrantAuditLogAccess(ctx, adminUser, user.ResourceTag()) @@ -328,10 +333,10 @@ func TestJWTGeneratorMakeLoginToken(t *testing.T) { generator := jimm.NewJWTGenerator(test.database, test.accessChecker, test.jwtService) generator.SetTags(mt, ct) - _, err := generator.MakeLoginToken(context.Background(), &openfga.User{ - Identity: &dbmodel.Identity{ - Name: test.username, - }, + i, err := dbmodel.NewIdentity(test.username) + c.Assert(err, qt.IsNil) + _, err = generator.MakeLoginToken(context.Background(), &openfga.User{ + Identity: i, }) if test.expectedError != "" { c.Assert(err, qt.ErrorMatches, test.expectedError) @@ -427,10 +432,10 @@ func TestJWTGeneratorMakeToken(t *testing.T) { ) generator.SetTags(mt, ct) - _, err := generator.MakeLoginToken(context.Background(), &openfga.User{ - Identity: &dbmodel.Identity{ - Name: "eve@canonical.com", - }, + i, err := dbmodel.NewIdentity("eve@canonical.com") + c.Assert(err, qt.IsNil) + _, err = generator.MakeLoginToken(context.Background(), &openfga.User{ + Identity: i, }) c.Assert(err, qt.IsNil) @@ -756,10 +761,10 @@ func createTestControllerEnvironment(ctx context.Context, c *qt.C, db db.Databas err = db.GetGroup(ctx, &group) c.Assert(err, qt.IsNil) - u := dbmodel.Identity{ - Name: petname.Generate(2, "-") + "@canonical.com", - } - c.Assert(db.DB.Create(&u).Error, qt.IsNil) + u, err := dbmodel.NewIdentity(petname.Generate(2, "-"+"canonical.com")) + c.Assert(err, qt.IsNil) + + c.Assert(db.DB.Create(u).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: petname.Generate(2, "-"), @@ -830,7 +835,7 @@ func createTestControllerEnvironment(ctx context.Context, c *qt.C, db db.Databas c.Assert(err, qt.IsNil) c.Assert(len(offer.UUID), qt.Equals, 36) - return u, group, controller, model, offer, cloud, cred + return *u, group, controller, model, offer, cloud, cred } func TestAddGroup(t *testing.T) { diff --git a/internal/jimm/applicationoffer.go b/internal/jimm/applicationoffer.go index 0dbf54311..2cb7f59b1 100644 --- a/internal/jimm/applicationoffer.go +++ b/internal/jimm/applicationoffer.go @@ -140,10 +140,14 @@ func (j *JIMM) Offer(ctx context.Context, user *openfga.User, offer AddApplicati if ownerId == "" { ownerId = user.Tag().Id() } + + identity, err := dbmodel.NewIdentity(ownerId) + if err != nil { + return errors.E(op, err) + } + owner := openfga.NewUser( - &dbmodel.Identity{ - Name: ownerId, - }, + identity, j.OpenFGAClient, ) if err := owner.SetApplicationOfferAccess(ctx, doc.ResourceTag(), ofganames.AdministratorRelation); err != nil { @@ -154,10 +158,13 @@ func (j *JIMM) Offer(ctx context.Context, user *openfga.User, offer AddApplicati zap.String("application-offer", doc.UUID)) } + everyoneIdentity, err := dbmodel.NewIdentity(ofganames.EveryoneUser) + if err != nil { + return errors.E(op, err) + } + everyone := openfga.NewUser( - &dbmodel.Identity{ - Name: ofganames.EveryoneUser, - }, + everyoneIdentity, j.OpenFGAClient, ) if err := everyone.SetApplicationOfferAccess(ctx, doc.ResourceTag(), ofganames.ReaderRelation); err != nil { @@ -357,8 +364,13 @@ func (j *JIMM) GetApplicationOffer(ctx context.Context, user *openfga.User, offe func (j *JIMM) GrantOfferAccess(ctx context.Context, user *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) error { const op = errors.Op("jimm.GrantOfferAccess") - err := j.doApplicationOfferAdmin(ctx, user, offerURL, func(offer *dbmodel.ApplicationOffer, api API) error { - tUser := openfga.NewUser(&dbmodel.Identity{Name: ut.Id()}, j.OpenFGAClient) + identity, err := dbmodel.NewIdentity(ut.Id()) + if err != nil { + return errors.E(op, err) + } + + err = j.doApplicationOfferAdmin(ctx, user, offerURL, func(offer *dbmodel.ApplicationOffer, api API) error { + tUser := openfga.NewUser(identity, j.OpenFGAClient) currentRelation := tUser.GetApplicationOfferAccess(ctx, offer.ResourceTag()) currentAccessLevel := ToOfferAccessString(currentRelation) targetAccessLevel := determineAccessLevelAfterGrant(currentAccessLevel, string(access)) @@ -414,8 +426,13 @@ func determineAccessLevelAfterGrant(currentAccessLevel, grantAccessLevel string) func (j *JIMM) RevokeOfferAccess(ctx context.Context, user *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) (err error) { const op = errors.Op("jimm.RevokeOfferAccess") + identity, err := dbmodel.NewIdentity(ut.Id()) + if err != nil { + return errors.E(op, err) + } + err = j.doApplicationOfferAdmin(ctx, user, offerURL, func(offer *dbmodel.ApplicationOffer, api API) error { - tUser := openfga.NewUser(&dbmodel.Identity{Name: ut.Id()}, j.OpenFGAClient) + tUser := openfga.NewUser(identity, j.OpenFGAClient) targetRelation, err := ToOfferRelation(string(access)) if err != nil { return errors.E(op, err) @@ -664,10 +681,12 @@ func (j *JIMM) applicationOfferFilters(ctx context.Context, jujuFilters ...jujup } if len(f.AllowedConsumerTags) > 0 { for _, u := range f.AllowedConsumerTags { - dbUser := dbmodel.Identity{ - Name: u, + identity, err := dbmodel.NewIdentity(u) + if err != nil { + return nil, errors.E(err) } - ofgaUser := openfga.NewUser(&dbUser, j.OpenFGAClient) + + ofgaUser := openfga.NewUser(identity, j.OpenFGAClient) offerUUIDs, err := ofgaUser.ListApplicationOffers(ctx, ofganames.ConsumerRelation) if err != nil { return nil, errors.E(err) diff --git a/internal/jimm/applicationoffer_test.go b/internal/jimm/applicationoffer_test.go index bbe88f622..3f995450f 100644 --- a/internal/jimm/applicationoffer_test.go +++ b/internal/jimm/applicationoffer_test.go @@ -43,47 +43,40 @@ var initializeEnvironment = func(c *qt.C, ctx context.Context, db *db.Database, env := environment{} // Alice is a model admin, but not a superuser or offer admin. - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } - c.Assert(db.DB.Create(&u).Error, qt.IsNil) + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u).Error, qt.IsNil) - u1 := dbmodel.Identity{ - Name: "eve@canonical.com", - } - c.Assert(db.DB.Create(&u1).Error, qt.IsNil) + u1, err := dbmodel.NewIdentity("eve@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u1).Error, qt.IsNil) - u2 := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(db.DB.Create(&u2).Error, qt.IsNil) + u2, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u2).Error, qt.IsNil) - u3 := dbmodel.Identity{ - Name: "fred@canonical.com", - } - c.Assert(db.DB.Create(&u3).Error, qt.IsNil) + u3, err := dbmodel.NewIdentity("fred@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u3).Error, qt.IsNil) - u4 := dbmodel.Identity{ - Name: "grant@canonical.com", - } - c.Assert(db.DB.Create(&u4).Error, qt.IsNil) + u4, err := dbmodel.NewIdentity("grant@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u4).Error, qt.IsNil) // Jane is an offer admin, but not a superuser or model admin. - u5 := dbmodel.Identity{ - Name: "jane@canonical.com", - } - c.Assert(db.DB.Create(&u5).Error, qt.IsNil) + u5, err := dbmodel.NewIdentity("jane@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u5).Error, qt.IsNil) // Joe is a superuser, but not a model or offer admin. - u6 := dbmodel.Identity{ - Name: "joe@canonical.com", - } - c.Assert(db.DB.Create(&u6).Error, qt.IsNil) + u6, err := dbmodel.NewIdentity("joe@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u6).Error, qt.IsNil) - err := openfga.NewUser(&u6, client).SetControllerAccess(ctx, names.NewControllerTag(jimmUUID), ofganames.AdministratorRelation) + err = openfga.NewUser(u6, client).SetControllerAccess(ctx, names.NewControllerTag(jimmUUID), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) - env.users = []dbmodel.Identity{u, u1, u2, u3, u4, u5, u6} + env.users = []dbmodel.Identity{*u, *u1, *u2, *u3, *u4, *u5, *u6} cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -96,7 +89,7 @@ var initializeEnvironment = func(c *qt.C, ctx context.Context, db *db.Database, env.clouds = []dbmodel.Cloud{cloud} // user u is administrator of the test-cloud - err = openfga.NewUser(&u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) controller := dbmodel.Controller{ @@ -147,7 +140,7 @@ var initializeEnvironment = func(c *qt.C, ctx context.Context, db *db.Database, env.models = []dbmodel.Model{model} // user u is administrator of the test-model - err = openfga.NewUser(&u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) err = client.AddControllerModel(context.Background(), controller.ResourceTag(), model.ResourceTag()) @@ -171,19 +164,19 @@ var initializeEnvironment = func(c *qt.C, ctx context.Context, db *db.Database, c.Assert(err, qt.IsNil) // user u1 is administrator of the test-offer - err = openfga.NewUser(&u1, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u1, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) // user u2 is consumer of the test-offer - err = openfga.NewUser(&u2, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.ConsumerRelation) + err = openfga.NewUser(u2, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.ConsumerRelation) c.Assert(err, qt.IsNil) // user u3 is reader of the test-offer - err = openfga.NewUser(&u3, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.ReaderRelation) + err = openfga.NewUser(u3, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.ReaderRelation) c.Assert(err, qt.IsNil) // user u5 is administrator of the test-offer - err = openfga.NewUser(&u5, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u5, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) return &env @@ -568,20 +561,17 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { err = db.Migrate(ctx, false) c.Assert(err, qt.IsNil) - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } - c.Assert(db.DB.Create(&u).Error, qt.IsNil) + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u).Error, qt.IsNil) - u1 := dbmodel.Identity{ - Name: "eve@canonical.com", - } - c.Assert(db.DB.Create(&u1).Error, qt.IsNil) + u1, err := dbmodel.NewIdentity("eve@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u1).Error, qt.IsNil) - u2 := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(db.DB.Create(&u2).Error, qt.IsNil) + u2, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u2).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -593,7 +583,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { c.Assert(db.DB.Create(&cloud).Error, qt.IsNil) // user u is administrator of the test-model - err = openfga.NewUser(&u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) controller := dbmodel.Controller{ @@ -647,24 +637,23 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { c.Assert(err, qt.IsNil) // user u is administrator of the test offer - err = openfga.NewUser(&u, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) // user u1 is reader of the test offer - err = openfga.NewUser(&u1, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.ReaderRelation) + err = openfga.NewUser(u1, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.ReaderRelation) c.Assert(err, qt.IsNil) // user u2 is consumer of the test offer - err = openfga.NewUser(&u2, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.ConsumerRelation) + err = openfga.NewUser(u2, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.ConsumerRelation) c.Assert(err, qt.IsNil) everyoneTag := names.NewUserTag(ofganames.EveryoneUser) - uAll := dbmodel.Identity{ - Name: everyoneTag.Id(), - } - c.Assert(db.DB.Create(&uAll).Error, qt.IsNil) + uAll, err := dbmodel.NewIdentity(everyoneTag.Id()) + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(uAll).Error, qt.IsNil) // user uAll is reader of the test offer - err = openfga.NewUser(&uAll, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.ReaderRelation) + err = openfga.NewUser(uAll, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.ReaderRelation) c.Assert(err, qt.IsNil) j := &jimm.JIMM{ @@ -729,7 +718,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { expectedError string }{{ about: "admin can get the application offer consume details ", - user: &u, + user: u, details: jujuparams.ConsumeOfferDetails{ Offer: &jujuparams.ApplicationOfferDetails{ OfferURL: "test-offer-url", @@ -784,7 +773,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { }, }, { about: "users with consume access can get the application offer consume details with filtered users", - user: &u2, + user: u2, details: jujuparams.ConsumeOfferDetails{ Offer: &jujuparams.ApplicationOfferDetails{ OfferURL: "test-offer-url", @@ -833,7 +822,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { }, }, { about: "user with read access cannot get application offer consume details", - user: &u1, + user: u1, details: jujuparams.ConsumeOfferDetails{ Offer: &jujuparams.ApplicationOfferDetails{ OfferURL: "test-offer-url", @@ -842,7 +831,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { expectedError: "unauthorized", }, { about: "no such offer", - user: &u, + user: u, details: jujuparams.ConsumeOfferDetails{ Offer: &jujuparams.ApplicationOfferDetails{ OfferURL: "no-such-offer", @@ -942,19 +931,16 @@ func TestGetApplicationOffer(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - u1 := dbmodel.Identity{ - Name: "eve@canonical.com", - } + u1, err := dbmodel.NewIdentity("eve@canonical.com") + c.Assert(err, qt.IsNil) c.Assert(j.Database.DB.Create(&u1).Error, qt.IsNil) - u2 := dbmodel.Identity{ - Name: "bob@canonical.com", - } + u2, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) c.Assert(j.Database.DB.Create(&u2).Error, qt.IsNil) cloud := dbmodel.Cloud{ @@ -1044,11 +1030,11 @@ func TestGetApplicationOffer(t *testing.T) { c.Assert(err, qt.IsNil) // user u is administrator of the test offer - err = openfga.NewUser(&u, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) // user u1 is reader of the test offer - err = openfga.NewUser(&u1, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.ReaderRelation) + err = openfga.NewUser(u1, client).SetApplicationOfferAccess(ctx, offer.ResourceTag(), ofganames.ReaderRelation) c.Assert(err, qt.IsNil) tests := []struct { @@ -1059,7 +1045,7 @@ func TestGetApplicationOffer(t *testing.T) { expectedError string }{{ about: "admin can get the application offer", - user: &u, + user: u, offerURL: "test-offer-url", expectedOfferDetails: jujuparams.ApplicationOfferAdminDetails{ ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ @@ -1110,7 +1096,7 @@ func TestGetApplicationOffer(t *testing.T) { }, }, { about: "user with read access can get the application offer, but users and connections are filtered", - user: &u1, + user: u1, offerURL: "test-offer-url", expectedOfferDetails: jujuparams.ApplicationOfferAdminDetails{ ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ @@ -1152,12 +1138,12 @@ func TestGetApplicationOffer(t *testing.T) { }, }, { about: "user without access cannot get the application offer", - user: &u2, + user: u2, offerURL: "test-offer-url", expectedError: "application offer not found", }, { about: "not found", - user: &u1, + user: u1, offerURL: "offer-not-found", expectedError: "application offer not found", }} @@ -1244,10 +1230,9 @@ func TestOffer(t *testing.T) { createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { ctx := context.Background() - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } - c.Assert(db.DB.Create(&u).Error, qt.IsNil) + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -1259,7 +1244,7 @@ func TestOffer(t *testing.T) { c.Assert(db.DB.Create(&cloud).Error, qt.IsNil) // user u is administrator of the test-cloud - err := openfga.NewUser(&u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) controller := dbmodel.Controller{ @@ -1299,7 +1284,7 @@ func TestOffer(t *testing.T) { c.Assert(err, qt.IsNil) // user u is administrator of the test-model - err = openfga.NewUser(&u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) offerParams := jimm.AddApplicationOfferParams{ @@ -1350,7 +1335,7 @@ func TestOffer(t *testing.T) { }}, } - return u, offerParams, offer, nil + return *u, offerParams, offer, nil }, }, { about: "controller returns an error when creating an offer", @@ -1366,10 +1351,9 @@ func TestOffer(t *testing.T) { createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { ctx := context.Background() - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } - c.Assert(db.DB.Create(&u).Error, qt.IsNil) + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -1381,7 +1365,7 @@ func TestOffer(t *testing.T) { c.Assert(db.DB.Create(&cloud).Error, qt.IsNil) // user u is administrator of the test-cloud - err := openfga.NewUser(&u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) controller := dbmodel.Controller{ @@ -1421,7 +1405,7 @@ func TestOffer(t *testing.T) { c.Assert(err, qt.IsNil) // user u is administrator of the test-model - err = openfga.NewUser(&u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) offerParams := jimm.AddApplicationOfferParams{ @@ -1436,7 +1420,7 @@ func TestOffer(t *testing.T) { offer := dbmodel.ApplicationOffer{} - return u, offerParams, offer, func(c *qt.C, err error) { + return *u, offerParams, offer, func(c *qt.C, err error) { c.Assert(err, qt.ErrorMatches, "a silly error") } }, @@ -1452,11 +1436,10 @@ func TestOffer(t *testing.T) { return nil }, createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) - c.Assert(db.DB.Create(&u).Error, qt.IsNil) + c.Assert(db.DB.Create(u).Error, qt.IsNil) offerParams := jimm.AddApplicationOfferParams{ ModelTag: names.NewModelTag("model-not-found"), OfferName: "test-app-offer", @@ -1469,7 +1452,7 @@ func TestOffer(t *testing.T) { offer := dbmodel.ApplicationOffer{} - return u, offerParams, offer, func(c *qt.C, err error) { + return *u, offerParams, offer, func(c *qt.C, err error) { c.Assert(err, qt.ErrorMatches, "model not found") } }, @@ -1487,10 +1470,10 @@ func TestOffer(t *testing.T) { createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { ctx := context.Background() - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } - c.Assert(db.DB.Create(&u).Error, qt.IsNil) + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + + c.Assert(db.DB.Create(u).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -1502,7 +1485,7 @@ func TestOffer(t *testing.T) { c.Assert(db.DB.Create(&cloud).Error, qt.IsNil) // user u is administrator of the test-cloud - err := openfga.NewUser(&u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) controller := dbmodel.Controller{ @@ -1542,7 +1525,7 @@ func TestOffer(t *testing.T) { c.Assert(err, qt.IsNil) // user u is administrator of the test-model - err = openfga.NewUser(&u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) offerParams := jimm.AddApplicationOfferParams{ @@ -1557,7 +1540,7 @@ func TestOffer(t *testing.T) { offer := dbmodel.ApplicationOffer{} - return u, offerParams, offer, func(c *qt.C, err error) { + return *u, offerParams, offer, func(c *qt.C, err error) { c.Assert(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) c.Assert(err, qt.ErrorMatches, "application test-app") } @@ -1576,15 +1559,13 @@ func TestOffer(t *testing.T) { createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { ctx := context.Background() - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } - c.Assert(db.DB.Create(&u).Error, qt.IsNil) + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u).Error, qt.IsNil) - u1 := dbmodel.Identity{ - Name: "eve@canonical.com", - } - c.Assert(db.DB.Create(&u1).Error, qt.IsNil) + u1, err := dbmodel.NewIdentity("eve@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u1).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -1596,7 +1577,7 @@ func TestOffer(t *testing.T) { c.Assert(db.DB.Create(&cloud).Error, qt.IsNil) // user u is administrator of the test-cloud - err := openfga.NewUser(&u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) controller := dbmodel.Controller{ @@ -1636,7 +1617,7 @@ func TestOffer(t *testing.T) { c.Assert(err, qt.IsNil) // user u is administrator of the test-model - err = openfga.NewUser(&u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) offerParams := jimm.AddApplicationOfferParams{ @@ -1651,7 +1632,7 @@ func TestOffer(t *testing.T) { offer := dbmodel.ApplicationOffer{} - return u1, offerParams, offer, func(c *qt.C, err error) { + return *u1, offerParams, offer, func(c *qt.C, err error) { c.Assert(err, qt.ErrorMatches, "unauthorized") } }, @@ -1669,10 +1650,9 @@ func TestOffer(t *testing.T) { createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { ctx := context.Background() - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } - c.Assert(db.DB.Create(&u).Error, qt.IsNil) + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -1684,7 +1664,7 @@ func TestOffer(t *testing.T) { c.Assert(db.DB.Create(&cloud).Error, qt.IsNil) // user u is administrator of the test-cloud - err := openfga.NewUser(&u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) controller := dbmodel.Controller{ @@ -1724,7 +1704,7 @@ func TestOffer(t *testing.T) { c.Assert(err, qt.IsNil) // user u is administrator of the test-model - err = openfga.NewUser(&u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) offerParams := jimm.AddApplicationOfferParams{ @@ -1739,7 +1719,7 @@ func TestOffer(t *testing.T) { offer := dbmodel.ApplicationOffer{} - return u, offerParams, offer, func(c *qt.C, err error) { + return *u, offerParams, offer, func(c *qt.C, err error) { c.Assert(err, qt.ErrorMatches, "a silly error") } }, @@ -1757,10 +1737,9 @@ func TestOffer(t *testing.T) { createEnv: func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { ctx := context.Background() - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } - c.Assert(db.DB.Create(&u).Error, qt.IsNil) + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(db.DB.Create(u).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud", @@ -1772,7 +1751,7 @@ func TestOffer(t *testing.T) { c.Assert(db.DB.Create(&cloud).Error, qt.IsNil) // user u is administrator of the test-cloud - err := openfga.NewUser(&u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) controller := dbmodel.Controller{ @@ -1812,7 +1791,7 @@ func TestOffer(t *testing.T) { c.Assert(err, qt.IsNil) // user u is administrator of the test-model - err = openfga.NewUser(&u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) offerParams := jimm.AddApplicationOfferParams{ @@ -1827,7 +1806,7 @@ func TestOffer(t *testing.T) { offer := dbmodel.ApplicationOffer{} - return u, offerParams, offer, func(c *qt.C, err error) { + return *u, offerParams, offer, func(c *qt.C, err error) { c.Assert(err, qt.ErrorMatches, "application offer already exists") c.Assert(errors.ErrorCode(err), qt.Equals, errors.CodeAlreadyExists) } @@ -1887,9 +1866,8 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { createEnv := func(c *qt.C, db db.Database, client *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) { ctx := context.Background() - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) c.Assert(db.DB.Create(&u).Error, qt.IsNil) cloud := dbmodel.Cloud{ @@ -1902,7 +1880,7 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { c.Assert(db.DB.Create(&cloud).Error, qt.IsNil) // user u is administrator of the test-cloud - err := openfga.NewUser(&u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) controller := dbmodel.Controller{ @@ -1942,7 +1920,7 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { c.Assert(err, qt.IsNil) // user u is administrator of the test-model - err = openfga.NewUser(&u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) + err = openfga.NewUser(u, client).SetModelAccess(ctx, model.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) offerParams := jimm.AddApplicationOfferParams{ @@ -1993,7 +1971,7 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { }}, } - return u, offerParams, offer, nil + return *u, offerParams, offer, nil } api := &jimmtest.API{ diff --git a/internal/jimm/cloud.go b/internal/jimm/cloud.go index 633bfdb3d..6006a31ff 100644 --- a/internal/jimm/cloud.go +++ b/internal/jimm/cloud.go @@ -25,10 +25,12 @@ func (j *JIMM) GetUserCloudAccess(ctx context.Context, user *openfga.User, cloud accessLevel := user.GetCloudAccess(ctx, cloud) if accessLevel == ofganames.NoRelation { everyoneTag := names.NewUserTag(ofganames.EveryoneUser) + everyoneIdentity, err := dbmodel.NewIdentity(everyoneTag.Id()) + if err != nil { + return "", err + } everyone := openfga.NewUser( - &dbmodel.Identity{ - Name: everyoneTag.Id(), - }, + everyoneIdentity, j.OpenFGAClient, ) accessLevel = everyone.GetCloudAccess(ctx, cloud) @@ -99,10 +101,12 @@ func (j *JIMM) ForEachUserCloud(ctx context.Context, user *openfga.User, f func( } // Also include "public" clouds - everyoneDB := dbmodel.Identity{ - Name: ofganames.EveryoneUser, + everyoneDB, err := dbmodel.NewIdentity(ofganames.EveryoneUser) + if err != nil { + return errors.E(op, err) } - everyone := openfga.NewUser(&everyoneDB, j.OpenFGAClient) + + everyone := openfga.NewUser(everyoneDB, j.OpenFGAClient) for _, cloud := range clouds { if seen[cloud.Name] { diff --git a/internal/jimm/cloud_test.go b/internal/jimm/cloud_test.go index 13ae82241..9a945cba1 100644 --- a/internal/jimm/cloud_test.go +++ b/internal/jimm/cloud_test.go @@ -40,25 +40,32 @@ func TestGetCloud(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) + aliceIdentity, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) alice := openfga.NewUser( - &dbmodel.Identity{ - Name: "alice@canonical.com", - }, + aliceIdentity, client, ) + + bobIdentity, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) bob := openfga.NewUser( - &dbmodel.Identity{ - Name: "bob@canonical.com", - }, + bobIdentity, + client, + ) + + charlieIdentity, err := dbmodel.NewIdentity("charlie@canonical.com") + c.Assert(err, qt.IsNil) + charlie := openfga.NewUser( + charlieIdentity, client, ) - charlie := openfga.NewUser(&dbmodel.Identity{Name: "charlie@canonical.com"}, client) // daphne is a jimm administrator + daphneIdentity, err := dbmodel.NewIdentity("daphne@canonical.com") + c.Assert(err, qt.IsNil) daphne := openfga.NewUser( - &dbmodel.Identity{ - Name: "daphne@canonical.com", - }, + daphneIdentity, client, ) err = daphne.SetControllerAccess( @@ -68,10 +75,10 @@ func TestGetCloud(t *testing.T) { ) c.Assert(err, qt.IsNil) + everyoneIdentity, err := dbmodel.NewIdentity(ofganames.EveryoneUser) + c.Assert(err, qt.IsNil) everyone := openfga.NewUser( - &dbmodel.Identity{ - Name: ofganames.EveryoneUser, - }, + everyoneIdentity, client, ) @@ -168,28 +175,39 @@ func TestForEachCloud(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) + aliceIdentity, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) alice := openfga.NewUser( - &dbmodel.Identity{Name: "alice@canonical.com"}, + aliceIdentity, client, ) + + bobIdentity, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) bob := openfga.NewUser( - &dbmodel.Identity{Name: "bob@canonical.com"}, + bobIdentity, client, ) + + charlieIdentity, err := dbmodel.NewIdentity("charlie@canonical.com") + c.Assert(err, qt.IsNil) charlie := openfga.NewUser( - &dbmodel.Identity{Name: "charlie@canonical.com"}, + charlieIdentity, client, ) + + daphneIdentity, err := dbmodel.NewIdentity("daphne@canonical.com") + c.Assert(err, qt.IsNil) daphne := openfga.NewUser( - &dbmodel.Identity{Name: "daphne@canonical.com"}, + daphneIdentity, client, ) daphne.JimmAdmin = true + everyoneIdentity, err := dbmodel.NewIdentity(ofganames.EveryoneUser) + c.Assert(err, qt.IsNil) everyone := openfga.NewUser( - &dbmodel.Identity{ - Name: ofganames.EveryoneUser, - }, + everyoneIdentity, client, ) diff --git a/internal/jimm/cloudcredential_test.go b/internal/jimm/cloudcredential_test.go index 2cd671d0c..0541c3b1b 100644 --- a/internal/jimm/cloudcredential_test.go +++ b/internal/jimm/cloudcredential_test.go @@ -44,13 +44,12 @@ func TestUpdateCloudCredential(t *testing.T) { about: "all ok", jimmAdmin: true, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - user := openfga.NewUser(&u, client) - err := user.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) + user := openfga.NewUser(u, client) + err = user.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) cloud := dbmodel.Cloud{ @@ -151,21 +150,21 @@ func TestUpdateCloudCredential(t *testing.T) { expectedCredential.Models = []dbmodel.Model{m} - return &u, arg, expectedCredential, "" + return u, arg, expectedCredential, "" }, }, { about: "update credential error returned by controller", jimmAdmin: true, updateCredentialErrors: []error{nil, errors.E("test error")}, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - user := openfga.NewUser(&u, client) + user := openfga.NewUser(u, client) - err := user.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) + err = user.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) cloud := dbmodel.Cloud{ @@ -241,7 +240,7 @@ func TestUpdateCloudCredential(t *testing.T) { AuthType: "test-auth-type", }, } - return &u, arg, dbmodel.CloudCredential{}, "test error" + return u, arg, dbmodel.CloudCredential{}, "test error" }, }, { about: "check credential error returned by controller", @@ -249,14 +248,14 @@ func TestUpdateCloudCredential(t *testing.T) { checkCredentialErrors: []error{errors.E("test error")}, updateCredentialErrors: []error{nil}, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - user := openfga.NewUser(&u, client) + user := openfga.NewUser(u, client) - err := user.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) + err = user.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) cloud := dbmodel.Cloud{ @@ -327,26 +326,26 @@ func TestUpdateCloudCredential(t *testing.T) { AuthType: "test-auth-type", }, } - return &u, arg, dbmodel.CloudCredential{}, "test error" + return u, arg, dbmodel.CloudCredential{}, "test error" }, }, { about: "user is controller superuser", jimmAdmin: true, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - alice := openfga.NewUser(&u, client) + alice := openfga.NewUser(u, client) alice.JimmAdmin = true - err := alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) + err = alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) + c.Assert(err, qt.IsNil) + + eve, err := dbmodel.NewIdentity("eve@canonical.com") c.Assert(err, qt.IsNil) - eve := dbmodel.Identity{ - Name: "eve@canonical.com", - } c.Assert(j.Database.DB.Create(&eve).Error, qt.IsNil) cloud := dbmodel.Cloud{ @@ -361,7 +360,7 @@ func TestUpdateCloudCredential(t *testing.T) { err = alice.SetCloudAccess(context.Background(), cloud.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) - e := openfga.NewUser(&eve, client) + e := openfga.NewUser(eve, client) err = e.SetCloudAccess(context.Background(), cloud.ResourceTag(), ofganames.CanAddModelRelation) c.Assert(err, qt.IsNil) @@ -436,7 +435,7 @@ func TestUpdateCloudCredential(t *testing.T) { m.CloudCredential = dbmodel.CloudCredential{} m.CloudRegion = dbmodel.CloudRegion{} - return &u, arg, dbmodel.CloudCredential{ + return u, arg, dbmodel.CloudCredential{ Name: "test-credential-1", CloudName: cloud.Name, Cloud: dbmodel.Cloud{ @@ -457,14 +456,14 @@ func TestUpdateCloudCredential(t *testing.T) { checkCredentialErrors: []error{errors.E("test error")}, jimmAdmin: true, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - user := openfga.NewUser(&u, client) + user := openfga.NewUser(u, client) - err := user.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) + err = user.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) cloud := dbmodel.Cloud{ @@ -564,20 +563,20 @@ func TestUpdateCloudCredential(t *testing.T) { m.CloudRegion = dbmodel.CloudRegion{} expectedCredential.Models = []dbmodel.Model{m} - return &u, arg, expectedCredential, "" + return u, arg, expectedCredential, "" }, }, { about: "skip update", jimmAdmin: true, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, jimm.UpdateCloudCredentialArgs, dbmodel.CloudCredential, string) { - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - user := openfga.NewUser(&u, client) + user := openfga.NewUser(u, client) - err := user.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) + err = user.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) cloud := dbmodel.Cloud{ @@ -670,7 +669,7 @@ func TestUpdateCloudCredential(t *testing.T) { m.CloudRegion = dbmodel.CloudRegion{} cred.Models = []dbmodel.Model{m} - return &u, arg, cred, "" + return u, arg, cred, "" }, }} for _, test := range tests { @@ -876,14 +875,14 @@ func TestRevokeCloudCredential(t *testing.T) { }{{ about: "credential revoked", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - alice := openfga.NewUser(&u, client) + alice := openfga.NewUser(u, client) - err := alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) + err = alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) cloud := dbmodel.Cloud{ @@ -941,7 +940,7 @@ func TestRevokeCloudCredential(t *testing.T) { } tag := names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1") - return &u, tag, "" + return u, tag, "" }, }, { about: "credential revoked - controller returns a not found error", @@ -950,14 +949,14 @@ func TestRevokeCloudCredential(t *testing.T) { Code: jujuparams.CodeNotFound, }}, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - alice := openfga.NewUser(&u, client) + alice := openfga.NewUser(u, client) - err := alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) + err = alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) cloud := dbmodel.Cloud{ @@ -1015,19 +1014,19 @@ func TestRevokeCloudCredential(t *testing.T) { } tag := names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1") - return &u, tag, "" + return u, tag, "" }, }, { about: "credential still used by a model", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - alice := openfga.NewUser(&u, client) + alice := openfga.NewUser(u, client) - err := alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) + err = alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) cloud := dbmodel.Cloud{ @@ -1090,37 +1089,37 @@ func TestRevokeCloudCredential(t *testing.T) { tag := names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1") - return &u, tag, `cloud credential still used by 1 model\(s\)` + return u, tag, `cloud credential still used by 1 model\(s\)` }, }, { about: "user not owner of credentials - unauthorizer error", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - alice := openfga.NewUser(&u, client) + alice := openfga.NewUser(u, client) - err := alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) + err = alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) tag := names.NewCloudCredentialTag("test-cloud/eve@canonical.com/test-credential-1") - return &u, tag, "unauthorized" + return u, tag, "unauthorized" }, }, { about: "error revoking credential on controller", revokeCredentialErrors: []error{errors.E("test error")}, createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, string) { - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - alice := openfga.NewUser(&u, client) + alice := openfga.NewUser(u, client) - err := alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) + err = alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) cloud := dbmodel.Cloud{ @@ -1174,7 +1173,7 @@ func TestRevokeCloudCredential(t *testing.T) { tag := names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1") - return &u, tag, "test error" + return u, tag, "test error" }, }} for _, test := range tests { @@ -1285,14 +1284,14 @@ func TestGetCloudCredential(t *testing.T) { }{{ about: "all ok", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, dbmodel.CloudCredential, string) { - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - alice := openfga.NewUser(&u, client) + alice := openfga.NewUser(u, client) - err := alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) + err = alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) cloud := dbmodel.Cloud{ @@ -1350,24 +1349,24 @@ func TestGetCloudCredential(t *testing.T) { tag := names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1") - return &u, tag, cred, "" + return u, tag, cred, "" }, }, { about: "credential not found", createEnv: func(c *qt.C, j *jimm.JIMM, client *openfga.OFGAClient) (*dbmodel.Identity, names.CloudCredentialTag, dbmodel.CloudCredential, string) { - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(&u).Error, qt.IsNil) - alice := openfga.NewUser(&u, client) + alice := openfga.NewUser(u, client) - err := alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) + err = alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) tag := names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1") - return &u, tag, dbmodel.CloudCredential{}, `cloudcredential "test-cloud/alice@canonical.com/test-credential-1" not found` + return u, tag, dbmodel.CloudCredential{}, `cloudcredential "test-cloud/alice@canonical.com/test-credential-1" not found` }, }} for _, test := range tests { diff --git a/internal/jimm/clouddefaults_test.go b/internal/jimm/clouddefaults_test.go index aaf64d095..869e4bfff 100644 --- a/internal/jimm/clouddefaults_test.go +++ b/internal/jimm/clouddefaults_test.go @@ -41,10 +41,9 @@ func TestSetCloudDefaults(t *testing.T) { }{{ about: "defaults do not exist yet - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + user, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(user).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud-1", @@ -63,7 +62,7 @@ func TestSetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ IdentityName: user.Name, - Identity: user, + Identity: *user, CloudID: cloud.ID, Cloud: cloud, Region: "test-region", @@ -71,7 +70,7 @@ func TestSetCloudDefaults(t *testing.T) { } return testConfig{ - user: &user, + user: user, cloud: names.NewCloudTag(cloud.Name), region: "test-region", defaults: defaults, @@ -81,10 +80,10 @@ func TestSetCloudDefaults(t *testing.T) { }, { about: "set defaults without region - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + user, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + + c.Assert(j.Database.DB.Create(user).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud-1", @@ -103,14 +102,14 @@ func TestSetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ IdentityName: user.Name, - Identity: user, + Identity: *user, CloudID: cloud.ID, Cloud: cloud, Defaults: defaults, } return testConfig{ - user: &user, + user: user, cloud: names.NewCloudTag(cloud.Name), region: "", defaults: defaults, @@ -120,10 +119,10 @@ func TestSetCloudDefaults(t *testing.T) { }, { about: "defaults already exist - defaults updated", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + user, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + + c.Assert(j.Database.DB.Create(user).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud-1", @@ -136,7 +135,7 @@ func TestSetCloudDefaults(t *testing.T) { j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ IdentityName: user.Name, - Identity: user, + Identity: *user, CloudID: cloud.ID, Cloud: cloud, Region: cloud.Regions[0].Name, @@ -155,7 +154,7 @@ func TestSetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ IdentityName: user.Name, - Identity: user, + Identity: *user, CloudID: cloud.ID, Cloud: cloud, Region: "test-region", @@ -163,7 +162,7 @@ func TestSetCloudDefaults(t *testing.T) { } return testConfig{ - user: &user, + user: user, cloud: names.NewCloudTag(cloud.Name), region: "test-region", defaults: defaults, @@ -173,10 +172,10 @@ func TestSetCloudDefaults(t *testing.T) { }, { about: "cloudregion does not exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + user, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + + c.Assert(j.Database.DB.Create(user).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud-1", @@ -193,7 +192,7 @@ func TestSetCloudDefaults(t *testing.T) { } return testConfig{ - user: &user, + user: user, cloud: names.NewCloudTag(cloud.Name), region: cloud.Regions[0].Name, defaults: defaults, @@ -203,10 +202,10 @@ func TestSetCloudDefaults(t *testing.T) { }, { about: "cannot set agent-version", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + user, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + + c.Assert(j.Database.DB.Create(user).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud-1", @@ -223,7 +222,7 @@ func TestSetCloudDefaults(t *testing.T) { } return testConfig{ - user: &user, + user: user, cloud: names.NewCloudTag(cloud.Name), region: cloud.Regions[0].Name, defaults: defaults, @@ -286,10 +285,10 @@ func TestUnsetCloudDefaults(t *testing.T) { }{{ about: "all ok - keys removed from the defaults map", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + user, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + + c.Assert(j.Database.DB.Create(user).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud-1", @@ -300,7 +299,7 @@ func TestUnsetCloudDefaults(t *testing.T) { } c.Assert(j.Database.DB.Create(&cloud).Error, qt.IsNil) - err := j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ + err = j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ IdentityName: user.Name, CloudID: cloud.ID, Region: cloud.Regions[0].Name, @@ -321,7 +320,7 @@ func TestUnsetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ IdentityName: user.Name, - Identity: user, + Identity: *user, CloudID: cloud.ID, Cloud: cloud, Region: "test-region", @@ -331,7 +330,7 @@ func TestUnsetCloudDefaults(t *testing.T) { } return testConfig{ - user: &user, + user: user, cloud: names.NewCloudTag(cloud.Name), region: "test-region", keys: keys, @@ -341,10 +340,10 @@ func TestUnsetCloudDefaults(t *testing.T) { }, { about: "unset without region - keys removed from the defaults map", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + user, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + + c.Assert(j.Database.DB.Create(user).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud-1", @@ -355,7 +354,7 @@ func TestUnsetCloudDefaults(t *testing.T) { } c.Assert(j.Database.DB.Create(&cloud).Error, qt.IsNil) - err := j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ + err = j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ IdentityName: user.Name, CloudID: cloud.ID, Defaults: map[string]interface{}{ @@ -375,7 +374,7 @@ func TestUnsetCloudDefaults(t *testing.T) { cloud.Regions = nil expectedDefaults := dbmodel.CloudDefaults{ IdentityName: user.Name, - Identity: user, + Identity: *user, CloudID: cloud.ID, Cloud: cloud, Region: "", @@ -385,7 +384,7 @@ func TestUnsetCloudDefaults(t *testing.T) { } return testConfig{ - user: &user, + user: user, cloud: names.NewCloudTag(cloud.Name), region: "", keys: keys, @@ -395,10 +394,10 @@ func TestUnsetCloudDefaults(t *testing.T) { }, { about: "cloudregiondefaults not found", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - user := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + user, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + + c.Assert(j.Database.DB.Create(user).Error, qt.IsNil) cloud := dbmodel.Cloud{ Name: "test-cloud-1", @@ -416,7 +415,7 @@ func TestUnsetCloudDefaults(t *testing.T) { } return testConfig{ - user: &user, + user: user, cloud: names.NewCloudTag(cloud.Name), region: cloud.Name, keys: keys, @@ -471,15 +470,13 @@ func TestModelDefaultsForCloud(t *testing.T) { err := j.Database.Migrate(ctx, true) c.Assert(err, qt.Equals, nil) - user := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&user).Error, qt.IsNil) + user, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(user).Error, qt.IsNil) - user1 := dbmodel.Identity{ - Name: "alice@canonical.com", - } - c.Assert(j.Database.DB.Create(&user1).Error, qt.IsNil) + user1, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(j.Database.DB.Create(user1).Error, qt.IsNil) cloud1 := dbmodel.Cloud{ Name: "test-cloud-1", @@ -547,7 +544,7 @@ func TestModelDefaultsForCloud(t *testing.T) { }) c.Assert(err, qt.Equals, nil) - result, err := j.ModelDefaultsForCloud(ctx, &user, names.NewCloudTag(cloud1.Name)) + result, err := j.ModelDefaultsForCloud(ctx, user, names.NewCloudTag(cloud1.Name)) c.Assert(err, qt.Equals, nil) c.Assert(result, qt.DeepEquals, jujuparams.ModelDefaultsResult{ Config: map[string]jujuparams.ModelDefaults{ @@ -581,7 +578,7 @@ func TestModelDefaultsForCloud(t *testing.T) { }, }) - result, err = j.ModelDefaultsForCloud(ctx, &user, names.NewCloudTag(cloud2.Name)) + result, err = j.ModelDefaultsForCloud(ctx, user, names.NewCloudTag(cloud2.Name)) c.Assert(err, qt.Equals, nil) c.Assert(result, qt.DeepEquals, jujuparams.ModelDefaultsResult{ Config: map[string]jujuparams.ModelDefaults{ @@ -610,7 +607,7 @@ func TestModelDefaultsForCloud(t *testing.T) { }, }) - result, err = j.ModelDefaultsForCloud(ctx, &user1, names.NewCloudTag(cloud2.Name)) + result, err = j.ModelDefaultsForCloud(ctx, user1, names.NewCloudTag(cloud2.Name)) c.Assert(err, qt.Equals, nil) c.Assert(result, qt.DeepEquals, jujuparams.ModelDefaultsResult{ Config: map[string]jujuparams.ModelDefaults{}, diff --git a/internal/jimm/controller.go b/internal/jimm/controller.go index cb0551d89..c1b42141d 100644 --- a/internal/jimm/controller.go +++ b/internal/jimm/controller.go @@ -168,10 +168,13 @@ func (j *JIMM) AddController(ctx context.Context, user *openfga.User, ctl *dbmod // it is available to all users. Other clouds require `juju grant-cloud` to add permissions. if cloud.ResourceTag().String() == ms.CloudTag { everyoneTag := names.NewUserTag(ofganames.EveryoneUser) + everyoneIdentity, err := dbmodel.NewIdentity(everyoneTag.Id()) + if err != nil { + zapctx.Error(ctx, "failed to create identity model", zap.Error(err)) + return errors.E(op, err) + } everyone := openfga.NewUser( - &dbmodel.Identity{ - Name: everyoneTag.Id(), - }, + everyoneIdentity, j.OpenFGAClient, ) if err := everyone.SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.CanAddModelRelation); err != nil { diff --git a/internal/jimm/controller_test.go b/internal/jimm/controller_test.go index 0fcfb42de..d9fc14af7 100644 --- a/internal/jimm/controller_test.go +++ b/internal/jimm/controller_test.go @@ -147,11 +147,10 @@ func TestAddController(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) - alice := openfga.NewUser(&u, client) + alice := openfga.NewUser(u, client) alice.JimmAdmin = true err = alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) @@ -315,10 +314,10 @@ func TestAddControllerWithVault(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } - alice := openfga.NewUser(&u, ofgaClient) + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + + alice := openfga.NewUser(u, ofgaClient) alice.JimmAdmin = true err = alice.SetControllerAccess(context.Background(), j.ResourceTag(), ofganames.AdministratorRelation) @@ -1435,10 +1434,10 @@ func TestInitiateMigration(t *testing.T) { }{{ about: "model migration initiated successfully", user: func(client *openfga.OFGAClient) *openfga.User { + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) return openfga.NewUser( - &dbmodel.Identity{ - Name: "alice@canonical.com", - }, + u, client, ) }, @@ -1460,10 +1459,10 @@ func TestInitiateMigration(t *testing.T) { }, { about: "model not found", user: func(client *openfga.OFGAClient) *openfga.User { + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) return openfga.NewUser( - &dbmodel.Identity{ - Name: "alice@canonical.com", - }, + u, client, ) }, @@ -1480,10 +1479,10 @@ func TestInitiateMigration(t *testing.T) { }, { about: "InitiateMigration call fails", user: func(client *openfga.OFGAClient) *openfga.User { + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) return openfga.NewUser( - &dbmodel.Identity{ - Name: "alice@canonical.com", - }, + u, client, ) }, @@ -1501,10 +1500,10 @@ func TestInitiateMigration(t *testing.T) { }, { about: "non-admin-user gets unauthorized error", user: func(client *openfga.OFGAClient) *openfga.User { + u, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) return openfga.NewUser( - &dbmodel.Identity{ - Name: "bob@canonical.com", - }, + u, client, ) }, @@ -1520,10 +1519,10 @@ func TestInitiateMigration(t *testing.T) { }, { about: "invalid model tag", user: func(client *openfga.OFGAClient) *openfga.User { + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) return openfga.NewUser( - &dbmodel.Identity{ - Name: "alice@canonical.com", - }, + u, client, ) }, @@ -1539,10 +1538,10 @@ func TestInitiateMigration(t *testing.T) { }, { about: "invalid target controller tag", user: func(client *openfga.OFGAClient) *openfga.User { + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) return openfga.NewUser( - &dbmodel.Identity{ - Name: "alice@canonical.com", - }, + u, client, ) }, @@ -1558,10 +1557,10 @@ func TestInitiateMigration(t *testing.T) { }, { about: "invalid target user tag", user: func(client *openfga.OFGAClient) *openfga.User { + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) return openfga.NewUser( - &dbmodel.Identity{ - Name: "alice@canonical.com", - }, + u, client, ) }, @@ -1577,10 +1576,10 @@ func TestInitiateMigration(t *testing.T) { }, { about: "invalid macaroon data", user: func(client *openfga.OFGAClient) *openfga.User { + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) return openfga.NewUser( - &dbmodel.Identity{ - Name: "alice@canonical.com", - }, + u, client, ) }, diff --git a/internal/jimm/identitymodeldefaults_test.go b/internal/jimm/identitymodeldefaults_test.go index 9741c1a27..b0561fb14 100644 --- a/internal/jimm/identitymodeldefaults_test.go +++ b/internal/jimm/identitymodeldefaults_test.go @@ -37,10 +37,10 @@ func TestSetIdentityModelDefaults(t *testing.T) { }{{ about: "defaults do not exist yet - defaults created", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - identity := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) + identity, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + + c.Assert(j.Database.DB.Create(identity).Error, qt.IsNil) defaults := map[string]interface{}{ "key1": float64(42), @@ -49,12 +49,12 @@ func TestSetIdentityModelDefaults(t *testing.T) { expectedDefaults := dbmodel.IdentityModelDefaults{ IdentityName: identity.Name, - Identity: identity, + Identity: *identity, Defaults: defaults, } return testConfig{ - identity: &identity, + identity: identity, defaults: defaults, expectedDefaults: &expectedDefaults, } @@ -62,14 +62,14 @@ func TestSetIdentityModelDefaults(t *testing.T) { }, { about: "defaults already exist - defaults updated", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - identity := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) + identity, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + + c.Assert(j.Database.DB.Create(identity).Error, qt.IsNil) j.Database.SetIdentityModelDefaults(ctx, &dbmodel.IdentityModelDefaults{ IdentityName: identity.Name, - Identity: identity, + Identity: *identity, Defaults: map[string]interface{}{ "key1": float64(17), "key2": "a test string", @@ -84,12 +84,12 @@ func TestSetIdentityModelDefaults(t *testing.T) { expectedDefaults := dbmodel.IdentityModelDefaults{ IdentityName: identity.Name, - Identity: identity, + Identity: *identity, Defaults: defaults, } return testConfig{ - identity: &identity, + identity: identity, defaults: defaults, expectedDefaults: &expectedDefaults, } @@ -97,10 +97,10 @@ func TestSetIdentityModelDefaults(t *testing.T) { }, { about: "cannot set agent-version", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - identity := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) + identity, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + + c.Assert(j.Database.DB.Create(identity).Error, qt.IsNil) defaults := map[string]interface{}{ "agent-version": "2.0", @@ -109,7 +109,7 @@ func TestSetIdentityModelDefaults(t *testing.T) { } return testConfig{ - identity: &identity, + identity: identity, defaults: defaults, expectedError: `agent-version cannot have a default value`, } @@ -163,27 +163,27 @@ func TestIdentityModelDefaults(t *testing.T) { }{{ about: "defaults do not exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - identity := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) + identity, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + + c.Assert(j.Database.DB.Create(identity).Error, qt.IsNil) return testConfig{ - identity: &identity, + identity: identity, expectedError: "identitymodeldefaults not found", } }, }, { about: "defaults exist", setup: func(c *qt.C, j *jimm.JIMM) testConfig { - identity := dbmodel.Identity{ - Name: "bob@canonical.com", - } - c.Assert(j.Database.DB.Create(&identity).Error, qt.IsNil) + identity, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + + c.Assert(j.Database.DB.Create(identity).Error, qt.IsNil) j.Database.SetIdentityModelDefaults(ctx, &dbmodel.IdentityModelDefaults{ IdentityName: identity.Name, - Identity: identity, + Identity: *identity, Defaults: map[string]interface{}{ "key1": float64(42), "key2": "a changed string", @@ -192,7 +192,7 @@ func TestIdentityModelDefaults(t *testing.T) { }) return testConfig{ - identity: &identity, + identity: identity, expectedDefaults: map[string]interface{}{ "key1": float64(42), "key2": "a changed string", diff --git a/internal/jimm/jimm_test.go b/internal/jimm/jimm_test.go index 690d5410a..8d2062e56 100644 --- a/internal/jimm/jimm_test.go +++ b/internal/jimm/jimm_test.go @@ -45,15 +45,23 @@ func TestFindAuditEvents(t *testing.T) { err = j.Database.Migrate(ctx, true) c.Assert(err, qt.Equals, nil) - admin := openfga.NewUser(&dbmodel.Identity{Name: "alice@canonical.com"}, client) + alice, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) + + admin := openfga.NewUser(alice, client) err = admin.SetControllerAccess(ctx, j.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) - privileged := openfga.NewUser(&dbmodel.Identity{Name: "bob@canonical.com"}, client) + bob, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) + + privileged := openfga.NewUser(bob, client) err = privileged.SetControllerAccess(ctx, j.ResourceTag(), ofganames.AuditLogViewerRelation) c.Assert(err, qt.IsNil) - unprivileged := openfga.NewUser(&dbmodel.Identity{Name: "eve@canonical.com"}, client) + eve, err := dbmodel.NewIdentity("eve@canonical.com") + c.Assert(err, qt.IsNil) + unprivileged := openfga.NewUser(eve, client) events := []dbmodel.AuditLogEntry{{ Time: now, diff --git a/internal/jimm/model.go b/internal/jimm/model.go index b0c725872..f83c70265 100644 --- a/internal/jimm/model.go +++ b/internal/jimm/model.go @@ -519,9 +519,11 @@ func (b *modelBuilder) JujuModelInfo() *jujuparams.ModelInfo { func (j *JIMM) AddModel(ctx context.Context, user *openfga.User, args *ModelCreateArgs) (_ *jujuparams.ModelInfo, err error) { const op = errors.Op("jimm.AddModel") - owner := &dbmodel.Identity{ - Name: args.Owner.Id(), + owner, err := dbmodel.NewIdentity(args.Owner.Id()) + if err != nil { + return nil, errors.E(op, err) } + err = j.Database.GetIdentity(ctx, owner) if err != nil { return nil, errors.E(op, err) diff --git a/internal/jimm/model_test.go b/internal/jimm/model_test.go index e4e92e37e..15cb110ff 100644 --- a/internal/jimm/model_test.go +++ b/internal/jimm/model_test.go @@ -942,12 +942,13 @@ func TestAddModel(t *testing.T) { if ownerId == "" { ownerId = user.Tag().Id() } + + ownerIdentity, err := dbmodel.NewIdentity(ownerId) + c.Assert(err, qt.IsNil) isModelAdmin, err := openfga.IsAdministrator( context.Background(), openfga.NewUser( - &dbmodel.Identity{ - Name: ownerId, - }, + ownerIdentity, client, ), m1.ResourceTag(), @@ -1296,9 +1297,9 @@ func TestModelInfo(t *testing.T) { env := jimmtest.ParseEnvironment(c, test.env) env.PopulateDBAndPermissions(c, j.ResourceTag(), j.Database, client) - dbUser := &dbmodel.Identity{ - Name: test.username, - } + dbUser, err := dbmodel.NewIdentity(test.username) + c.Assert(err, qt.IsNil) + user := openfga.NewUser(dbUser, client) mi, err := j.ModelInfo(context.Background(), user, names.NewModelTag(test.uuid)) diff --git a/internal/jimm/service_account_test.go b/internal/jimm/service_account_test.go index 12bcec6cf..56dbbe496 100644 --- a/internal/jimm/service_account_test.go +++ b/internal/jimm/service_account_test.go @@ -28,11 +28,11 @@ func TestAddServiceAccount(t *testing.T) { OpenFGAClient: client, } c.Assert(err, qt.IsNil) + + bob, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, qt.IsNil) user := openfga.NewUser( - &dbmodel.Identity{ - Name: "bob@canonical.com", - DisplayName: "Bob", - }, + bob, client, ) clientID := "39caae91-b914-41ae-83f8-c7b86ca5ad5a@serviceaccount" @@ -40,11 +40,11 @@ func TestAddServiceAccount(t *testing.T) { c.Assert(err, qt.IsNil) err = j.AddServiceAccount(ctx, user, clientID) c.Assert(err, qt.IsNil) + + alive, err := dbmodel.NewIdentity("alive@canonical.com") + c.Assert(err, qt.IsNil) userAlice := openfga.NewUser( - &dbmodel.Identity{ - Name: "alive@canonical.com", - DisplayName: "Alice", - }, + alive, client, ) err = j.AddServiceAccount(ctx, userAlice, clientID) diff --git a/internal/jimm/user.go b/internal/jimm/user.go index f9975569c..79a5a2584 100644 --- a/internal/jimm/user.go +++ b/internal/jimm/user.go @@ -13,15 +13,16 @@ import ( // GetOpenFGAUserAndAuthorise returns a valid OpenFGA user, authorising // them as an admin of JIMM if a tuple exists for this user. -func (j *JIMM) GetOpenFGAUserAndAuthorise(ctx context.Context, email string) (*openfga.User, error) { +func (j *JIMM) GetOpenFGAUserAndAuthorise(ctx context.Context, emailOrSvcAccId string) (*openfga.User, error) { const op = errors.Op("jimm.GetOpenFGAUserAndAuthorise") + // TODO(ale8k): Name is email for NOW until we add email field + // and map emails/usernames to a uuid for the user. Then, queries should be + // queried upon by uuid, not username. // Setup identity model using the tag to populate query fields - user := &dbmodel.Identity{ - // TODO(ale8k): Name is email for NOW until we add email field - // and map emails/usernames to a uuid for the user. Then, queries should be - // queried upon by uuid, not username. - Name: email, + user, err := dbmodel.NewIdentity(emailOrSvcAccId) + if err != nil { + return nil, errors.E(op, err) } // Load the users details @@ -64,13 +65,15 @@ func (j *JIMM) GetOpenFGAUserAndAuthorise(ctx context.Context, email string) (*o func (j *JIMM) GetUser(ctx context.Context, username string) (*openfga.User, error) { const op = errors.Op("jimm.GetUser") - user := dbmodel.Identity{ - Name: username, + user, err := dbmodel.NewIdentity(username) + if err != nil { + return nil, errors.E(op, err) } - if err := j.Database.GetIdentity(ctx, &user); err != nil { + + if err := j.Database.GetIdentity(ctx, user); err != nil { return nil, err } - u := openfga.NewUser(&user, j.OpenFGAClient) + u := openfga.NewUser(user, j.OpenFGAClient) isJimmAdmin, err := openfga.IsAdministrator(ctx, u, j.ResourceTag()) if err != nil { diff --git a/internal/jimm/user_test.go b/internal/jimm/user_test.go index bae5f7b52..564bfe429 100644 --- a/internal/jimm/user_test.go +++ b/internal/jimm/user_test.go @@ -62,7 +62,7 @@ func TestGetOpenFGAUser(t *testing.T) { // Username -> email c.Assert(ofgaUser.Name, qt.Equals, "bob@canonical.com.com") // As no display name was set for this user as they're being created this time over - c.Assert(ofgaUser.DisplayName, qt.Equals, "") + c.Assert(ofgaUser.DisplayName, qt.Equals, "bob") // The last login should be updated, so we check if it's been updated // in the last second (for general accuracy when testing) c.Assert((time.Since(ofgaUser.LastLogin.Time) > time.Second), qt.IsFalse) @@ -85,7 +85,7 @@ func TestGetOpenFGAUser(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(ofgaUser.Name, qt.Equals, "bob@canonical.com.com") - c.Assert(ofgaUser.DisplayName, qt.Equals, "") + c.Assert(ofgaUser.DisplayName, qt.Equals, "bob") c.Assert((time.Since(ofgaUser.LastLogin.Time) > time.Second), qt.IsFalse) c.Assert(ofgaUser.LastLogin.Valid, qt.IsTrue) // This user SHOULD be an admin, so ensure admin check is OK diff --git a/internal/jimmhttp/auth_handler_test.go b/internal/jimmhttp/auth_handler_test.go index 926194b57..e91adf84b 100644 --- a/internal/jimmhttp/auth_handler_test.go +++ b/internal/jimmhttp/auth_handler_test.go @@ -48,7 +48,12 @@ func TestBrowserLoginAndLogout(t *testing.T) { pgSession := sessionStore.(*pgstore.PGStore) pgSession.Cleanup(time.Nanosecond) - cookie, jimmHTTPServer, err := jimmtest.RunBrowserLoginAndKeepServerRunning(db, sessionStore) + cookie, jimmHTTPServer, err := jimmtest.RunBrowserLoginAndKeepServerRunning( + db, + sessionStore, + jimmtest.HardcodedSafeUsername, + jimmtest.HardcodedSafePassword, + ) c.Assert(err, qt.IsNil) defer jimmHTTPServer.Close() c.Assert(cookie, qt.Not(qt.Equals), "") diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index 378b8a3a9..31097a4fc 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -156,13 +156,13 @@ func SetupTestDashboardCallbackHandler(browserURL string, db *db.Database, sessi return s, nil } -func RunBrowserLoginAndKeepServerRunning(db *db.Database, sessionStore sessions.Store) (string, *httptest.Server, error) { - cookieString, jimmHTTPServer, err := runBrowserLogin(db, sessionStore) +func RunBrowserLoginAndKeepServerRunning(db *db.Database, sessionStore sessions.Store, username, password string) (string, *httptest.Server, error) { + cookieString, jimmHTTPServer, err := runBrowserLogin(db, sessionStore, username, password) return cookieString, jimmHTTPServer, err } -func RunBrowserLogin(db *db.Database, sessionStore sessions.Store) (string, error) { - cookieString, jimmHTTPServer, err := runBrowserLogin(db, sessionStore) +func RunBrowserLogin(db *db.Database, sessionStore sessions.Store, username, password string) (string, error) { + cookieString, jimmHTTPServer, err := runBrowserLogin(db, sessionStore, username, password) defer jimmHTTPServer.Close() return cookieString, err } @@ -174,7 +174,7 @@ func ParseCookies(cookies string) []*http.Cookie { return request.Cookies() } -func runBrowserLogin(db *db.Database, sessionStore sessions.Store) (string, *httptest.Server, error) { +func runBrowserLogin(db *db.Database, sessionStore sessions.Store, username, password string) (string, *httptest.Server, error) { var cookieString string // Setup final test redirect url server, to emulate @@ -228,8 +228,8 @@ func runBrowserLogin(db *db.Database, sessionStore sessions.Store) (string, *htt loginFormUrl := match[1] v := url.Values{} - v.Add("username", "jimm-test") - v.Add("password", "password") + v.Add("username", username) + v.Add("password", password) loginResp, err := client.PostForm(loginFormUrl, v) if err != nil { return cookieString, s, err diff --git a/internal/jimmtest/env.go b/internal/jimmtest/env.go index 335a2d474..c069773c4 100644 --- a/internal/jimmtest/env.go +++ b/internal/jimmtest/env.go @@ -181,10 +181,14 @@ func (m Model) addModelRelations(c *qt.C, jimmTag names.ControllerTag, db db.Dat // addControllerRelations adds permissions the model should have and adds permissions for users to the controller. func (ctl Controller) addControllerRelations(c *qt.C, jimmTag names.ControllerTag, db db.Database, client *openfga.OFGAClient) { if ctl.dbo.AdminIdentityName != "" { - user := openfga.NewUser(&dbmodel.Identity{ - Name: ctl.dbo.AdminIdentityName, - }, client) - err := user.SetControllerAccess(context.Background(), ctl.dbo.ResourceTag(), ofganames.AdministratorRelation) + userIdentity, err := dbmodel.NewIdentity(ctl.dbo.AdminIdentityName) + c.Assert(err, qt.IsNil) + + user := openfga.NewUser( + userIdentity, + client, + ) + err = user.SetControllerAccess(context.Background(), ctl.dbo.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) } err := client.AddCloudController(context.Background(), names.NewCloudTag(ctl.Cloud), ctl.dbo.ResourceTag()) diff --git a/internal/jimmtest/keycloak.go b/internal/jimmtest/keycloak.go index 005ce950b..26c42716c 100644 --- a/internal/jimmtest/keycloak.go +++ b/internal/jimmtest/keycloak.go @@ -20,6 +20,15 @@ import ( // These constants are based on the `docker-compose.yaml` and `local/keycloak/jimm-realm.json` content. const ( + // HardcodedSafeUsername is a hardcoded test keycloak user that pre-exists + // but is safe for use in a Juju UserTag when the email is retrieved. + HardcodedSafeUsername = "jimm-test" + HardcodedSafePassword = "password" + // HardcodedUnsafeUsername is a hardcoded test keycloak user that pre-exists + // but is unsafe for use in a Juju UserTag when the email is retrieved. + HardcodedUnsafeUsername = "jimm_test" + HardcodedUnsafePassword = "password" + keycloakHost = "localhost:8082" keycloakJIMMRealmPath = "/admin/realms/jimm" keycloakAdminUsername = "jimm" diff --git a/internal/jimmtest/suite.go b/internal/jimmtest/suite.go index 691ad6016..564826c42 100644 --- a/internal/jimmtest/suite.go +++ b/internal/jimmtest/suite.go @@ -90,10 +90,12 @@ func (s *JIMMSuite) SetUpTest(c *gc.C) { err = s.JIMM.Database.Migrate(ctx, false) c.Assert(err, gc.Equals, nil) - s.AdminUser = &dbmodel.Identity{ - Name: "alice@canonical.com", - LastLogin: db.Now(), - } + + alice, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, gc.IsNil) + alice.LastLogin = db.Now() + s.AdminUser = alice + err = s.JIMM.Database.GetIdentity(ctx, s.AdminUser) c.Assert(err, gc.Equals, nil) @@ -161,18 +163,18 @@ func (s *JIMMSuite) setupMacaroonDischarger(c *gc.C) *discharger.MacaroonDischar } func (s *JIMMSuite) AddAdminUser(c *gc.C, email string) { - identity := dbmodel.Identity{ - Name: email, - } - err := s.JIMM.Database.GetIdentity(context.Background(), &identity) + identity, err := dbmodel.NewIdentity(email) + c.Assert(err, gc.IsNil) + + err = s.JIMM.Database.GetIdentity(context.Background(), identity) c.Assert(err, gc.IsNil) // Set the display name of the identity. displayName, _, _ := strings.Cut(email, "@") identity.DisplayName = displayName - err = s.JIMM.Database.UpdateIdentity(context.Background(), &identity) + err = s.JIMM.Database.UpdateIdentity(context.Background(), identity) c.Assert(err, gc.IsNil) // Give the identity admin permission. - ofgaUser := openfga.NewUser(&identity, s.OFGAClient) + ofgaUser := openfga.NewUser(identity, s.OFGAClient) err = ofgaUser.SetControllerAccess(context.Background(), s.JIMM.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, gc.IsNil) } @@ -207,11 +209,11 @@ func (s *JIMMSuite) AddController(c *gc.C, name string, info *api.Info) { func (s *JIMMSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, cred jujuparams.CloudCredential) { ctx := context.Background() - u := dbmodel.Identity{ - Name: tag.Owner().Id(), - } - user := openfga.NewUser(&u, s.JIMM.OpenFGAClient) - err := s.JIMM.Database.GetIdentity(ctx, &u) + u, err := dbmodel.NewIdentity(tag.Owner().Id()) + c.Assert(err, gc.IsNil) + + user := openfga.NewUser(u, s.JIMM.OpenFGAClient) + err = s.JIMM.Database.GetIdentity(ctx, u) c.Assert(err, gc.Equals, nil) _, err = s.JIMM.UpdateCloudCredential(ctx, user, jimm.UpdateCloudCredentialArgs{ CredentialTag: tag, @@ -223,12 +225,13 @@ func (s *JIMMSuite) UpdateCloudCredential(c *gc.C, tag names.CloudCredentialTag, func (s *JIMMSuite) AddModel(c *gc.C, owner names.UserTag, name string, cloud names.CloudTag, region string, cred names.CloudCredentialTag) names.ModelTag { ctx := context.Background() - u := dbmodel.Identity{ - Name: owner.Id(), - } - err := s.JIMM.Database.GetIdentity(ctx, &u) + + u, err := dbmodel.NewIdentity(owner.Id()) + c.Assert(err, gc.IsNil) + + err = s.JIMM.Database.GetIdentity(ctx, u) c.Assert(err, gc.Equals, nil) - mi, err := s.JIMM.AddModel(ctx, s.NewUser(&u), &jimm.ModelCreateArgs{ + mi, err := s.JIMM.AddModel(ctx, s.NewUser(u), &jimm.ModelCreateArgs{ Name: name, Owner: owner, Cloud: cloud, @@ -237,7 +240,7 @@ func (s *JIMMSuite) AddModel(c *gc.C, owner names.UserTag, name string, cloud na }) c.Assert(err, gc.Equals, nil) - user := s.NewUser(&u) + user := s.NewUser(u) err = user.SetModelAccess(context.Background(), names.NewModelTag(mi.UUID), ofganames.AdministratorRelation) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/access_control_test.go b/internal/jujuapi/access_control_test.go index 551d1c82c..9d1b02fb1 100644 --- a/internal/jujuapi/access_control_test.go +++ b/internal/jujuapi/access_control_test.go @@ -1347,10 +1347,10 @@ func createTestControllerEnvironment(ctx context.Context, c *gc.C, s *accessCont err = db.GetGroup(ctx, &group) c.Assert(err, gc.IsNil) - u := dbmodel.Identity{ - Name: petname.Generate(2, "-") + "@canonical.com", - } - c.Assert(db.DB.Create(&u).Error, gc.IsNil) + u, err := dbmodel.NewIdentity(petname.Generate(2, "-") + "@canonical.com") + c.Assert(err, gc.IsNil) + + c.Assert(db.DB.Create(u).Error, gc.IsNil) cloud := dbmodel.Cloud{ Name: petname.Generate(2, "-"), @@ -1424,7 +1424,7 @@ func createTestControllerEnvironment(ctx context.Context, c *gc.C, s *accessCont conn := s.open(c, nil, "alice") client := api.NewClient(conn) - return u, group, controller, model, offer, cloud, cred, client, func() { + return *u, group, controller, model, offer, cloud, cred, client, func() { conn.Close() } } diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index 514fc6769..247a6db79 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -86,8 +86,29 @@ func (s *adminSuite) TestLoginToController(c *gc.C) { // We only test happy path here due to having tested edge cases and failure cases // within the auth service itself such as invalid cookies, expired access tokens and // missing/expired/revoked refresh tokens. +func (s *adminSuite) TestBrowserLoginWithSafeEmail(c *gc.C) { + testBrowserLogin( + c, + s, + jimmtest.HardcodedSafeUsername, + jimmtest.HardcodedSafePassword, + "user-jimm-test@canonical.com", + "jimm-test", + ) +} -func (s *adminSuite) TestBrowserLogin(c *gc.C) { +func (s *adminSuite) TestBrowserLoginWithUnsafeEmail(c *gc.C) { + testBrowserLogin( + c, + s, + jimmtest.HardcodedUnsafeUsername, + jimmtest.HardcodedUnsafePassword, + "user-jimm-test43cc8c@canonical.com", + "jimm-test43cc8c", + ) +} + +func testBrowserLogin(c *gc.C, s *adminSuite, username, password, expectedEmail, expectedDisplayName string) { // The setup runs a browser login with callback, ultimately retrieving // a logged in user by cookie. sqldb, err := s.JIMM.DB().DB.DB() @@ -96,7 +117,12 @@ func (s *adminSuite) TestBrowserLogin(c *gc.C) { sessionStore, err := pgstore.NewPGStoreFromPool(sqldb, []byte("secretsecretdigletts")) c.Assert(err, gc.IsNil) - cookie, err := jimmtest.RunBrowserLogin(s.JIMM.DB(), sessionStore) + cookie, err := jimmtest.RunBrowserLogin( + s.JIMM.DB(), + sessionStore, + username, + password, + ) c.Assert(err, gc.IsNil) c.Assert(cookie, gc.Not(gc.Equals), "") @@ -127,8 +153,8 @@ func (s *adminSuite) TestBrowserLogin(c *gc.C) { err = conn.APICall("Admin", 4, "", "LoginWithSessionCookie", nil, lr) c.Assert(err, gc.IsNil) - c.Assert(lr.UserInfo.Identity, gc.Equals, "user-jimm-test@canonical.com") - c.Assert(lr.UserInfo.DisplayName, gc.Equals, "jimm-test") + c.Assert(lr.UserInfo.Identity, gc.Equals, expectedEmail) + c.Assert(lr.UserInfo.DisplayName, gc.Equals, expectedDisplayName) } // TestBrowserLoginNoCookie attempts to login without a cookie. @@ -241,9 +267,9 @@ func (s *adminSuite) TestDeviceLogin(c *gc.C) { c.Assert(loginResult.UserInfo.DisplayName, gc.Equals, strings.Split(user.Email, "@")[0]) // Finally, ensure db did indeed update the access token for this user - updatedUser := &dbmodel.Identity{ - Name: user.Email, - } + updatedUser, err := dbmodel.NewIdentity(user.Email) + c.Assert(err, gc.IsNil) + c.Assert(s.JIMM.DB().GetIdentity(context.Background(), updatedUser), gc.IsNil) // TODO(ale8k): Do we need to validate the token again for the test? // It has just been through a verifier etc and was returned directly diff --git a/internal/jujuapi/applicationoffers_test.go b/internal/jujuapi/applicationoffers_test.go index 57d2c5204..10624bd1c 100644 --- a/internal/jujuapi/applicationoffers_test.go +++ b/internal/jujuapi/applicationoffers_test.go @@ -349,7 +349,10 @@ func (s *applicationOffersSuite) TestDestroyOffers(c *gc.C) { } err = s.JIMM.Database.GetApplicationOffer(context.Background(), &offer) c.Assert(err, gc.Equals, nil) - charlie := openfga.NewUser(&dbmodel.Identity{Name: "charlie@canonical.com"}, s.OFGAClient) + + charlieIdentity, err := dbmodel.NewIdentity("charlie@canonical.com") + c.Assert(err, gc.IsNil) + charlie := openfga.NewUser(charlieIdentity, s.OFGAClient) err = charlie.SetApplicationOfferAccess(context.Background(), offer.ResourceTag(), ofganames.ReaderRelation) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/cloud_test.go b/internal/jujuapi/cloud_test.go index 41990d849..224eb7334 100644 --- a/internal/jujuapi/cloud_test.go +++ b/internal/jujuapi/cloud_test.go @@ -942,17 +942,17 @@ func (s *cloudSuite) TestListCloudInfo(c *gc.C) { }, false) c.Assert(err, gc.Equals, nil) - /* - err = client.GrantCloud("bob@canonical.com", "add-model", "test-cloud") - c.Assert(err, gc.Equals, nil) - */ - bob := openfga.NewUser(&dbmodel.Identity{Name: "bob@canonical.com"}, s.OFGAClient) + bobIdentity, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, gc.IsNil) + bob := openfga.NewUser(bobIdentity, s.OFGAClient) err = bob.SetCloudAccess(context.Background(), names.NewCloudTag("test-cloud"), ofganames.CanAddModelRelation) c.Assert(err, gc.Equals, nil) err = bob.SetCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), ofganames.CanAddModelRelation) c.Assert(err, gc.Equals, nil) - alice := openfga.NewUser(&dbmodel.Identity{Name: "alice@canonical.com"}, s.OFGAClient) + aliceIdentity, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, gc.IsNil) + alice := openfga.NewUser(aliceIdentity, s.OFGAClient) err = alice.SetCloudAccess(context.Background(), names.NewCloudTag(jimmtest.TestCloudName), ofganames.CanAddModelRelation) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/jimm_test.go b/internal/jujuapi/jimm_test.go index 9d9014e2f..b875c6677 100644 --- a/internal/jujuapi/jimm_test.go +++ b/internal/jujuapi/jimm_test.go @@ -567,10 +567,11 @@ func (s *jimmSuite) TestImportModel(c *gc.C) { func (s *jimmSuite) TestAddCloudToController(c *gc.C) { ctx := context.Background() - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } - err := s.JIMM.Database.GetIdentity(ctx, &u) + + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, gc.IsNil) + + err = s.JIMM.Database.GetIdentity(ctx, u) c.Assert(err, gc.IsNil) conn := s.open(c, nil, "alice@canonical.com") @@ -594,7 +595,7 @@ func (s *jimmSuite) TestAddCloudToController(c *gc.C) { err = conn.APICall("JIMM", 4, "", "AddCloudToController", &req, nil) c.Assert(err, gc.Equals, nil) - user := openfga.NewUser(&u, s.OFGAClient) + user := openfga.NewUser(u, s.OFGAClient) cloud, err := s.JIMM.GetCloud(context.Background(), user, names.NewCloudTag("test-cloud")) c.Assert(err, gc.IsNil) @@ -604,10 +605,11 @@ func (s *jimmSuite) TestAddCloudToController(c *gc.C) { func (s *jimmSuite) TestAddExistingCloudToController(c *gc.C) { ctx := context.Background() - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } - err := s.JIMM.Database.GetIdentity(ctx, &u) + + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, gc.IsNil) + + err = s.JIMM.Database.GetIdentity(ctx, u) c.Assert(err, gc.IsNil) conn := s.open(c, nil, "alice@canonical.com") @@ -631,7 +633,7 @@ func (s *jimmSuite) TestAddExistingCloudToController(c *gc.C) { } err = conn.APICall("JIMM", 4, "", "AddCloudToController", &req, nil) c.Assert(err, gc.Equals, nil) - user := openfga.NewUser(&u, s.OFGAClient) + user := openfga.NewUser(u, s.OFGAClient) cloud, err := s.JIMM.GetCloud(context.Background(), user, names.NewCloudTag("test-cloud")) c.Assert(err, gc.IsNil) c.Assert(cloud.Name, gc.DeepEquals, "test-cloud") @@ -652,10 +654,11 @@ func (s *jimmSuite) TestAddExistingCloudToController(c *gc.C) { func (s *jimmSuite) TestRemoveCloudFromController(c *gc.C) { ctx := context.Background() - u := dbmodel.Identity{ - Name: "alice@canonical.com", - } - err := s.JIMM.Database.GetIdentity(ctx, &u) + + u, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, gc.IsNil) + + err = s.JIMM.Database.GetIdentity(ctx, u) c.Assert(err, gc.IsNil) conn := s.open(c, nil, "alice@canonical.com") @@ -679,7 +682,7 @@ func (s *jimmSuite) TestRemoveCloudFromController(c *gc.C) { err = conn.APICall("JIMM", 4, "", "AddCloudToController", &req, nil) c.Assert(err, gc.Equals, nil) - user := openfga.NewUser(&u, s.OFGAClient) + user := openfga.NewUser(u, s.OFGAClient) _, err = s.JIMM.GetCloud(context.Background(), user, names.NewCloudTag("test-cloud")) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/modelmanager_test.go b/internal/jujuapi/modelmanager_test.go index f2f9e5c16..a8b6f7260 100644 --- a/internal/jujuapi/modelmanager_test.go +++ b/internal/jujuapi/modelmanager_test.go @@ -264,8 +264,11 @@ func (s *modelManagerSuite) TestModelInfo(c *gc.C) { //client := modelmanager.NewClient(conn) //err := client.GrantModel("bob@canonical.com", "write", mt4.Id()) //c.Assert(err, gc.Equals, nil) - bob := openfga.NewUser(&dbmodel.Identity{Name: "bob@canonical.com"}, s.OFGAClient) - err := bob.SetModelAccess(context.Background(), mt4, ofganames.WriterRelation) + + bobIdentity, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, gc.IsNil) + bob := openfga.NewUser(bobIdentity, s.OFGAClient) + err = bob.SetModelAccess(context.Background(), mt4, ofganames.WriterRelation) c.Assert(err, gc.Equals, nil) mt5 := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-5", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, s.Model2.CloudCredential.ResourceTag()) @@ -521,7 +524,9 @@ func (s *modelManagerSuite) TestModelInfoDisableControllerUUIDMasking(c *gc.C) { s.AddAdminUser(c, "bob@canonical.com") // we make bob a jimm administrator - bob := openfga.NewUser(&dbmodel.Identity{Name: "bob@canonical.com"}, s.OFGAClient) + bobIdentity, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, gc.IsNil) + bob := openfga.NewUser(bobIdentity, s.OFGAClient) err = bob.SetControllerAccess(context.Background(), s.JIMM.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/usermanager_test.go b/internal/jujuapi/usermanager_test.go index bd322aa1e..54fd5aeeb 100644 --- a/internal/jujuapi/usermanager_test.go +++ b/internal/jujuapi/usermanager_test.go @@ -103,7 +103,7 @@ func (s *userManagerSuite) TestUserInfoWithDomain(c *gc.C) { users[0].DateCreated = time.Time{} c.Assert(users[0], jc.DeepEquals, jujuparams.UserInfo{ Username: "alice@mydomain", - DisplayName: "", + DisplayName: "alice", Access: "", LastConnection: users[0].LastConnection, }) diff --git a/internal/jujuapi/websocket_test.go b/internal/jujuapi/websocket_test.go index 833d8fd39..7a6da625a 100644 --- a/internal/jujuapi/websocket_test.go +++ b/internal/jujuapi/websocket_test.go @@ -78,10 +78,11 @@ func (s *websocketSuite) SetUpTest(c *gc.C) { err = s.JIMM.Database.GetModel(ctx, s.Model3) c.Assert(err, gc.Equals, nil) + bobIdentity, err := dbmodel.NewIdentity("bob@canonical.com") + c.Assert(err, gc.IsNil) + bob := openfga.NewUser( - &dbmodel.Identity{ - Name: "bob@canonical.com", - }, + bobIdentity, s.OFGAClient, ) err = bob.SetModelAccess(ctx, s.Model3.ResourceTag(), ofganames.ReaderRelation) diff --git a/internal/openfga/user.go b/internal/openfga/user.go index 23b30dcc3..c0a780942 100644 --- a/internal/openfga/user.go +++ b/internal/openfga/user.go @@ -450,7 +450,11 @@ func ListUsersWithAccess[T ofganames.ResourceTagger](ctx context.Context, client if entity.ID == "*" { entity.ID = ofganames.EveryoneUser } - users[i] = NewUser(&dbmodel.Identity{Name: entity.ID}, client) + identity, err := dbmodel.NewIdentity(entity.ID) + if err != nil { + zapctx.Error(ctx, "failed to return user with access", zap.Error(err), zap.String("id", entity.ID)) + } + users[i] = NewUser(identity, client) } return users, nil } diff --git a/internal/openfga/user_test.go b/internal/openfga/user_test.go index a4a843289..ea03167b1 100644 --- a/internal/openfga/user_test.go +++ b/internal/openfga/user_test.go @@ -53,10 +53,10 @@ func (s *userTestSuite) TestIsAdministrator(c *gc.C) { err := s.ofgaClient.AddRelation(ctx, userToGroup, groupToController) c.Assert(err, gc.IsNil) + uIdentity, err := dbmodel.NewIdentity(user.Id()) + c.Assert(err, gc.IsNil) u := openfga.NewUser( - &dbmodel.Identity{ - Name: user.Id(), - }, + uIdentity, s.ofgaClient, ) @@ -102,9 +102,16 @@ func (s *userTestSuite) TestModelAccess(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.Identity{Name: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.Identity{Name: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.ofgaClient) + adamIdentity, err := dbmodel.NewIdentity("adam") + c.Assert(err, gc.IsNil) + eveIdentity, err := dbmodel.NewIdentity(eve.Id()) + c.Assert(err, gc.IsNil) + aliceIdentity, err := dbmodel.NewIdentity(alice.Id()) + c.Assert(err, gc.IsNil) + + adamUser := openfga.NewUser(adamIdentity, s.ofgaClient) + eveUser := openfga.NewUser(eveIdentity, s.ofgaClient) + aliceUser := openfga.NewUser(aliceIdentity, s.ofgaClient) relation := eveUser.GetModelAccess(ctx, model) c.Assert(relation, gc.DeepEquals, ofganames.AdministratorRelation) @@ -137,9 +144,16 @@ func (s *userTestSuite) TestSetModelAccess(c *gc.C) { eve := names.NewUserTag("eve") alice := names.NewUserTag("alice") - adamUser := openfga.NewUser(&dbmodel.Identity{Name: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.Identity{Name: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.ofgaClient) + adamIdentity, err := dbmodel.NewIdentity("adam") + c.Assert(err, gc.IsNil) + eveIdentity, err := dbmodel.NewIdentity(eve.Id()) + c.Assert(err, gc.IsNil) + aliceIdentity, err := dbmodel.NewIdentity(alice.Id()) + c.Assert(err, gc.IsNil) + + adamUser := openfga.NewUser(adamIdentity, s.ofgaClient) + eveUser := openfga.NewUser(eveIdentity, s.ofgaClient) + aliceUser := openfga.NewUser(aliceIdentity, s.ofgaClient) err = eveUser.SetModelAccess(ctx, model, ofganames.AdministratorRelation) c.Assert(err, gc.IsNil) @@ -196,10 +210,16 @@ func (s *userTestSuite) TestCloudAccess(c *gc.C) { }} err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) + i, err := dbmodel.NewIdentity("adam") + c.Assert(err, gc.IsNil) + eveIdentity, err := dbmodel.NewIdentity(eve.Id()) + c.Assert(err, gc.IsNil) + aliceIdentity, err := dbmodel.NewIdentity(alice.Id()) + c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.Identity{Name: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.Identity{Name: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.ofgaClient) + adamUser := openfga.NewUser(i, s.ofgaClient) + eveUser := openfga.NewUser(eveIdentity, s.ofgaClient) + aliceUser := openfga.NewUser(aliceIdentity, s.ofgaClient) relation := eveUser.GetCloudAccess(ctx, cloud) c.Assert(relation, gc.DeepEquals, ofganames.AdministratorRelation) @@ -220,9 +240,16 @@ func (s *userTestSuite) TestSetCloudAccess(c *gc.C) { eve := names.NewUserTag("eve") alice := names.NewUserTag("alice") - adamUser := openfga.NewUser(&dbmodel.Identity{Name: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.Identity{Name: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.ofgaClient) + adamIdentity, err := dbmodel.NewIdentity("adam") + c.Assert(err, gc.IsNil) + eveIdentity, err := dbmodel.NewIdentity(eve.Id()) + c.Assert(err, gc.IsNil) + aliceIdentity, err := dbmodel.NewIdentity(alice.Id()) + c.Assert(err, gc.IsNil) + + adamUser := openfga.NewUser(adamIdentity, s.ofgaClient) + eveUser := openfga.NewUser(eveIdentity, s.ofgaClient) + aliceUser := openfga.NewUser(aliceIdentity, s.ofgaClient) err = eveUser.SetCloudAccess(ctx, cloud, ofganames.AdministratorRelation) c.Assert(err, gc.IsNil) @@ -273,9 +300,16 @@ func (s *userTestSuite) TestControllerAccess(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.Identity{Name: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.Identity{Name: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.ofgaClient) + adamIdentity, err := dbmodel.NewIdentity("adam") + c.Assert(err, gc.IsNil) + eveIdentity, err := dbmodel.NewIdentity(eve.Id()) + c.Assert(err, gc.IsNil) + aliceIdentity, err := dbmodel.NewIdentity(alice.Id()) + c.Assert(err, gc.IsNil) + + adamUser := openfga.NewUser(adamIdentity, s.ofgaClient) + eveUser := openfga.NewUser(eveIdentity, s.ofgaClient) + aliceUser := openfga.NewUser(aliceIdentity, s.ofgaClient) relation := eveUser.GetControllerAccess(ctx, controller) c.Assert(relation, gc.DeepEquals, ofganames.AdministratorRelation) @@ -302,9 +336,16 @@ func (s *userTestSuite) TestSetControllerAccess(c *gc.C) { eve := names.NewUserTag("eve") alice := names.NewUserTag("alice") - adamUser := openfga.NewUser(&dbmodel.Identity{Name: "adam"}, s.ofgaClient) - eveUser := openfga.NewUser(&dbmodel.Identity{Name: eve.Id()}, s.ofgaClient) - aliceUser := openfga.NewUser(&dbmodel.Identity{Name: alice.Id()}, s.ofgaClient) + adamIdentity, err := dbmodel.NewIdentity("adam") + c.Assert(err, gc.IsNil) + eveIdentity, err := dbmodel.NewIdentity(eve.Id()) + c.Assert(err, gc.IsNil) + aliceIdentity, err := dbmodel.NewIdentity(alice.Id()) + c.Assert(err, gc.IsNil) + + adamUser := openfga.NewUser(adamIdentity, s.ofgaClient) + eveUser := openfga.NewUser(eveIdentity, s.ofgaClient) + aliceUser := openfga.NewUser(aliceIdentity, s.ofgaClient) err = eveUser.SetControllerAccess(ctx, controller, ofganames.AdministratorRelation) c.Assert(err, gc.IsNil) @@ -339,7 +380,10 @@ func (s *userTestSuite) TestUnsetAuditLogViewerAccess(c *gc.C) { c.Assert(err, gc.IsNil) controller := names.NewControllerTag(controllerUUID.String()) - aliceUser := openfga.NewUser(&dbmodel.Identity{Name: "alice"}, s.ofgaClient) + aliceIdentity, err := dbmodel.NewIdentity("alice") + c.Assert(err, gc.IsNil) + + aliceUser := openfga.NewUser(aliceIdentity, s.ofgaClient) tuples := []openfga.Tuple{{ Object: ofganames.ConvertTag(aliceUser.Identity.ResourceTag()), @@ -425,7 +469,10 @@ func (s *userTestSuite) TestListRelatedUsers(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - eveUser := openfga.NewUser(&dbmodel.Identity{Name: "eve"}, s.ofgaClient) + eveIdentity, err := dbmodel.NewIdentity("eve") + c.Assert(err, gc.IsNil) + + eveUser := openfga.NewUser(eveIdentity, s.ofgaClient) isAdministrator, err := openfga.IsAdministrator(ctx, eveUser, offer) c.Assert(err, gc.IsNil) c.Assert(isAdministrator, gc.Equals, true) @@ -473,7 +520,10 @@ func (s *userTestSuite) TestListModels(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.Identity{Name: adam.Name()}, s.ofgaClient) + adamIdentity, err := dbmodel.NewIdentity(adam.Name()) + c.Assert(err, gc.IsNil) + + adamUser := openfga.NewUser(adamIdentity, s.ofgaClient) modelUUIDs, err := adamUser.ListModels(ctx, ofganames.ReaderRelation) c.Assert(err, gc.IsNil) wantUUIDs := []string{model1UUID.String(), model2UUID.String(), model3UUID.String()} @@ -515,7 +565,10 @@ func (s *userTestSuite) TestListApplicationOffers(c *gc.C) { err = s.ofgaClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) - adamUser := openfga.NewUser(&dbmodel.Identity{Name: adam.Name()}, s.ofgaClient) + adamIdentity, err := dbmodel.NewIdentity(adam.Name()) + c.Assert(err, gc.IsNil) + + adamUser := openfga.NewUser(adamIdentity, s.ofgaClient) offerUUIDs, err := adamUser.ListApplicationOffers(ctx, ofganames.ReaderRelation) c.Assert(err, gc.IsNil) wantUUIDs := []string{offer1UUID.String(), offer2UUID.String(), offer3UUID.String()} diff --git a/internal/rpc/proxy_test.go b/internal/rpc/proxy_test.go index 24338df48..eb5f1aff9 100644 --- a/internal/rpc/proxy_test.go +++ b/internal/rpc/proxy_test.go @@ -381,10 +381,12 @@ func (j *mockJIMM) OAuthAuthenticationService() jimm.OAuthAuthenticator { } func (j *mockJIMM) GetOpenFGAUserAndAuthorise(ctx context.Context, email string) (*openfga.User, error) { + identity, err := dbmodel.NewIdentity(email) + if err != nil { + return nil, err + } return openfga.NewUser( - &dbmodel.Identity{ - Name: email, - }, + identity, nil, ), nil } diff --git a/local/keycloak/jimm-realm.json b/local/keycloak/jimm-realm.json index ae1e5b0cf..2da30f9d6 100644 --- a/local/keycloak/jimm-realm.json +++ b/local/keycloak/jimm-realm.json @@ -476,6 +476,28 @@ "manage-account" ] } + }, + { + "id": "8281cec3-5b48-46eb-a41d-72c15ec3f9e1", + "username": "jimm_test", + "email": "jimm_test@canonical.com", + "emailVerified": true, + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "password" + } + ], + "realmRoles": [ + "user" + ], + "clientRoles": { + "account": [ + "view-profile", + "manage-account" + ] + } } ], "scopeMappings": [ diff --git a/local/seed_db/main.go b/local/seed_db/main.go index 65c44b52e..1eb5da824 100644 --- a/local/seed_db/main.go +++ b/local/seed_db/main.go @@ -47,10 +47,8 @@ func main() { os.Exit(1) } - u := dbmodel.Identity{ - Name: petname.Generate(2, "-") + "@canonical.com", - } - if err = db.DB.Create(&u).Error; err != nil { + u, _ := dbmodel.NewIdentity(petname.Generate(2, "-") + "@canonical.com") + if err = db.DB.Create(u).Error; err != nil { fmt.Println("failed to add user to db ", err) os.Exit(1) } diff --git a/service.go b/service.go index 9c1d12847..4a4f0e9d2 100644 --- a/service.go +++ b/service.go @@ -556,7 +556,11 @@ func ensureControllerAdministrators(ctx context.Context, client *openfga.OFGACli tuples := []openfga.Tuple{} for _, username := range admins { userTag := names.NewUserTag(username) - user := openfga.NewUser(&dbmodel.Identity{Name: userTag.Id()}, client) + i, err := dbmodel.NewIdentity(userTag.Id()) + if err != nil { + return errors.E(err) + } + user := openfga.NewUser(i, client) isAdmin, err := openfga.IsAdministrator(ctx, user, controller) if err != nil { return errors.E(err) diff --git a/service_test.go b/service_test.go index 17fd9d846..b0fa86112 100644 --- a/service_test.go +++ b/service_test.go @@ -240,8 +240,10 @@ func TestOpenFGA(t *testing.T) { // assert controller admins have been created in openfga for _, username := range []string{"alice", "eve"} { + i, err := dbmodel.NewIdentity(username) + c.Assert(err, qt.IsNil) user := openfga.NewUser( - &dbmodel.Identity{Name: username}, + i, client, ) allowed, err := openfga.IsAdministrator(context.Background(), user, names.NewControllerTag(p.ControllerUUID)) @@ -278,9 +280,8 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { UUID: "7e4e7ffb-5116-4544-a400-f584d08c410e", Name: "test-application-offer", } - user := dbmodel.Identity{ - Name: "alice@canonical.com", - } + user, err := dbmodel.NewIdentity("alice@canonical.com") + c.Assert(err, qt.IsNil) ctx := context.Background() @@ -350,7 +351,7 @@ func TestThirdPartyCaveatDischarge(t *testing.T) { }) if test.setup != nil { - test.setup(c, ofgaClient, &user) + test.setup(c, ofgaClient, user) } m, err := bakery.NewMacaroon( From 2c54221059ce868c070890d7f9fd9a7e50f0c374 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Tue, 23 Apr 2024 08:57:21 +0100 Subject: [PATCH 107/126] Oauth2 test fix (#1193) co-authored babak --- internal/auth/oauth2_test.go | 3 ++- internal/dbmodel/identity.go | 9 ++++++++- internal/dbmodel/sql/postgres/1_6.sql | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 1281b058b..c63bda76b 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -421,7 +421,8 @@ func TestAuthenticateBrowserSessionHandlesExpiredAccessTokens(t *testing.T) { previousToken := u.AccessToken u.AccessTokenExpiry = time.Now() - db.UpdateIdentity(ctx, u) + err = db.UpdateIdentity(ctx, u) + c.Assert(err, qt.IsNil) ctx, err = authSvc.AuthenticateBrowserSession(ctx, rec, req) c.Assert(err, qt.IsNil) diff --git a/internal/dbmodel/identity.go b/internal/dbmodel/identity.go index a5f83de31..5fecb14ae 100644 --- a/internal/dbmodel/identity.go +++ b/internal/dbmodel/identity.go @@ -70,7 +70,14 @@ type Identity struct { RefreshToken string // AccessTokenExpiry is the expiration date for this access token. - AccessTokenExpiry time.Time + // + // Note: + // We're using gorm type to dictate the timezone, such that the + // database doesn't drop the time zone part for the access token, + // and then on retrievals we can perform a safe check for the validity + // based on the timezone that was initially sent with the token + // from the authentication server. + AccessTokenExpiry time.Time `gorm:"type:timestamp with time zone"` // AccessTokenType is the type for the token, typically bearer. AccessTokenType string diff --git a/internal/dbmodel/sql/postgres/1_6.sql b/internal/dbmodel/sql/postgres/1_6.sql index 5f380c483..b3d4bd28e 100644 --- a/internal/dbmodel/sql/postgres/1_6.sql +++ b/internal/dbmodel/sql/postgres/1_6.sql @@ -2,7 +2,7 @@ -- and is a migration that renames `user` to `identity`. ALTER TABLE users ADD COLUMN access_token TEXT; ALTER TABLE users ADD COLUMN refresh_token TEXT; -ALTER TABLE users ADD COLUMN access_token_expiry TIMESTAMP; +ALTER TABLE users ADD COLUMN access_token_expiry TIMESTAMP WITH TIME ZONE; ALTER TABLE users ADD COLUMN access_token_type TEXT; -- Note that we don't need to rename underlying indexes/constraints. As Postgres From 4d6c777718711a4bf95b34ae55585042dfe6005b Mon Sep 17 00:00:00 2001 From: alesstimec Date: Tue, 23 Apr 2024 12:29:21 +0200 Subject: [PATCH 108/126] Call UserInfo instead of verifying ID token. --- internal/auth/oauth2.go | 42 +++++-------------------------- internal/auth/oauth2_test.go | 12 ++------- internal/jimm/admin.go | 7 +----- internal/jimm/jimm.go | 10 +++----- internal/jimmhttp/auth_handler.go | 14 +++-------- internal/rpc/proxy_test.go | 10 +------- 6 files changed, 16 insertions(+), 79 deletions(-) diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index 4eee890f0..9585b68b5 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -224,53 +224,23 @@ func (as *AuthenticationService) DeviceAccessToken(ctx context.Context, res *oau oauth2.SetAuthURLParam("client_secret", as.oauthConfig.ClientSecret), ) if err != nil { + zapctx.Error(ctx, "device access token call failed", zap.Error(err)) return nil, errors.E(op, err, "device access token call failed") } return t, nil } -// ExtractAndVerifyIDToken extracts the id token from the extras claims of an oauth2 token -// and performs signature verification of the token. -func (as *AuthenticationService) ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) { +// UserInfo call the user info endpoint using the provided token and returns user's email. +func (as *AuthenticationService) UserInfo(ctx context.Context, oauth2Token *oauth2.Token) (string, error) { const op = errors.Op("auth.AuthenticationService.ExtractAndVerifyIDToken") - // Extract the ID Token from oauth2 token. - rawIDToken, ok := oauth2Token.Extra("id_token").(string) - if !ok { - return nil, errors.E(op, "failed to extract id token") - } - - verifier := as.provider.Verifier(&oidc.Config{ - ClientID: as.oauthConfig.ClientID, - }) - - token, err := verifier.Verify(ctx, rawIDToken) + userInfo, err := as.provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token)) if err != nil { - zapctx.Error(ctx, "failed to verify id token", zap.Error(err)) - return nil, errors.E(op, err, "failed to verify id token") - } - - return token, nil -} - -// Email retrieves the users email from an id token via the email claim -func (as *AuthenticationService) Email(idToken *oidc.IDToken) (string, error) { - const op = errors.Op("auth.AuthenticationService.Email") - - var claims struct { - Email string `json:"email"` - EmailVerified bool `json:"email_verified"` // TODO(ale8k): Add verification logic - } - if idToken == nil { - return "", errors.E(op, "id token is nil") - } - - if err := idToken.Claims(&claims); err != nil { - return "", errors.E(op, err, "failed to extract claims") + return "", errors.E(op, err, "failed to retrieve user info") } - return claims.Email, nil + return userInfo.Email, nil } // MintSessionToken mints a session token to be used when logging into JIMM diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 634ecd882..85724cf4f 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -146,16 +146,8 @@ func TestDevice(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(token, qt.IsNotNil) - // Extract and verify id token - idToken, err := authSvc.ExtractAndVerifyIDToken(ctx, token) - c.Assert(err, qt.IsNil) - c.Assert(idToken, qt.IsNotNil) - - // Test subject set - c.Assert(idToken.Subject, qt.Equals, u.Id) - - // Retrieve the email - email, err := authSvc.Email(idToken) + // Get user info + email, err := authSvc.UserInfo(ctx, token) c.Assert(err, qt.IsNil) c.Assert(email, qt.Equals, u.Email) diff --git a/internal/jimm/admin.go b/internal/jimm/admin.go index d39f97b1a..60d2a69f7 100644 --- a/internal/jimm/admin.go +++ b/internal/jimm/admin.go @@ -39,12 +39,7 @@ func GetDeviceSessionToken(ctx context.Context, authenticator OAuthAuthenticator return "", errors.E(op, err) } - idToken, err := authenticator.ExtractAndVerifyIDToken(ctx, token) - if err != nil { - return "", errors.E(op, err) - } - - email, err := authenticator.Email(idToken) + email, err := authenticator.UserInfo(ctx, token) if err != nil { return "", errors.E(op, err) } diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index fbf22e097..20d1fc79f 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -11,7 +11,6 @@ import ( "strings" "time" - "github.com/coreos/go-oidc/v3/oidc" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/juju/api/base" "github.com/juju/juju/core/crossmodel" @@ -137,12 +136,9 @@ type OAuthAuthenticator interface { // See Device(...) godoc for more info pertaining to the flow. DeviceAccessToken(ctx context.Context, res *oauth2.DeviceAuthResponse) (*oauth2.Token, error) - // ExtractAndVerifyIDToken extracts the id token from the extras claims of an oauth2 token - // and performs signature verification of the token. - ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) - - // Email retrieves the users email from an id token via the email claim - Email(idToken *oidc.IDToken) (string, error) + // UserInfo calls the user info endpoint using the provided token and returns + // user's email. + UserInfo(ctx context.Context, oauth2Token *oauth2.Token) (string, error) // MintSessionToken mints a session token to be used when logging into JIMM // via an access token. The token only contains the user's email for authentication. diff --git a/internal/jimmhttp/auth_handler.go b/internal/jimmhttp/auth_handler.go index c23b22dce..cfaed76d0 100644 --- a/internal/jimmhttp/auth_handler.go +++ b/internal/jimmhttp/auth_handler.go @@ -5,7 +5,6 @@ import ( "encoding/json" "net/http" - "github.com/coreos/go-oidc/v3/oidc" "github.com/go-chi/chi/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" @@ -50,8 +49,7 @@ type OAuthHandlerParams struct { type BrowserOAuthAuthenticator interface { AuthCodeURL() string Exchange(ctx context.Context, code string) (*oauth2.Token, error) - ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) - Email(idToken *oidc.IDToken) (string, error) + UserInfo(ctx context.Context, oauth2Token *oauth2.Token) (string, error) UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error CreateBrowserSession( ctx context.Context, @@ -119,15 +117,9 @@ func (oah *OAuthHandler) Callback(w http.ResponseWriter, r *http.Request) { return } - idToken, err := authSvc.ExtractAndVerifyIDToken(ctx, token) + email, err := authSvc.UserInfo(ctx, token) if err != nil { - writeError(ctx, w, http.StatusBadRequest, err, "failed to extract and verify id token") - return - } - - email, err := authSvc.Email(idToken) - if err != nil { - writeError(ctx, w, http.StatusBadRequest, err, "failed to extract email from id token") + writeError(ctx, w, http.StatusBadRequest, err, "failed to retrieve user info") return } diff --git a/internal/rpc/proxy_test.go b/internal/rpc/proxy_test.go index 24338df48..fa54f428e 100644 --- a/internal/rpc/proxy_test.go +++ b/internal/rpc/proxy_test.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" @@ -321,14 +320,7 @@ func (m *mockOAuthAuthenticator) DeviceAccessToken(ctx context.Context, res *oau return &oauth2.Token{}, nil } -func (m *mockOAuthAuthenticator) ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) { - if m.err != nil { - return nil, m.err - } - return &oidc.IDToken{}, nil -} - -func (m *mockOAuthAuthenticator) Email(idToken *oidc.IDToken) (string, error) { +func (m *mockOAuthAuthenticator) UserInfo(ctx context.Context, oauth2Token *oauth2.Token) (string, error) { if m.err != nil { return "", m.err } From a6a02381e9374a8c5e1495ce3fbcaec8c315b752 Mon Sep 17 00:00:00 2001 From: Ales Stimec Date: Wed, 24 Apr 2024 11:52:38 +0200 Subject: [PATCH 109/126] Revert "Call UserInfo instead of verifying ID token." --- internal/auth/oauth2.go | 42 ++++++++++++++++++++++++++----- internal/auth/oauth2_test.go | 12 +++++++-- internal/jimm/admin.go | 7 +++++- internal/jimm/jimm.go | 10 +++++--- internal/jimmhttp/auth_handler.go | 14 ++++++++--- internal/rpc/proxy_test.go | 10 +++++++- 6 files changed, 79 insertions(+), 16 deletions(-) diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index bc448dd24..aa3e70bed 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -223,23 +223,53 @@ func (as *AuthenticationService) DeviceAccessToken(ctx context.Context, res *oau oauth2.SetAuthURLParam("client_secret", as.oauthConfig.ClientSecret), ) if err != nil { - zapctx.Error(ctx, "device access token call failed", zap.Error(err)) return nil, errors.E(op, err, "device access token call failed") } return t, nil } -// UserInfo call the user info endpoint using the provided token and returns user's email. -func (as *AuthenticationService) UserInfo(ctx context.Context, oauth2Token *oauth2.Token) (string, error) { +// ExtractAndVerifyIDToken extracts the id token from the extras claims of an oauth2 token +// and performs signature verification of the token. +func (as *AuthenticationService) ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) { const op = errors.Op("auth.AuthenticationService.ExtractAndVerifyIDToken") - userInfo, err := as.provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token)) + // Extract the ID Token from oauth2 token. + rawIDToken, ok := oauth2Token.Extra("id_token").(string) + if !ok { + return nil, errors.E(op, "failed to extract id token") + } + + verifier := as.provider.Verifier(&oidc.Config{ + ClientID: as.oauthConfig.ClientID, + }) + + token, err := verifier.Verify(ctx, rawIDToken) if err != nil { - return "", errors.E(op, err, "failed to retrieve user info") + zapctx.Error(ctx, "failed to verify id token", zap.Error(err)) + return nil, errors.E(op, err, "failed to verify id token") + } + + return token, nil +} + +// Email retrieves the users email from an id token via the email claim +func (as *AuthenticationService) Email(idToken *oidc.IDToken) (string, error) { + const op = errors.Op("auth.AuthenticationService.Email") + + var claims struct { + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` // TODO(ale8k): Add verification logic + } + if idToken == nil { + return "", errors.E(op, "id token is nil") + } + + if err := idToken.Claims(&claims); err != nil { + return "", errors.E(op, err, "failed to extract claims") } - return userInfo.Email, nil + return claims.Email, nil } // MintSessionToken mints a session token to be used when logging into JIMM diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 9c8b522b9..c63bda76b 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -146,8 +146,16 @@ func TestDevice(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(token, qt.IsNotNil) - // Get user info - email, err := authSvc.UserInfo(ctx, token) + // Extract and verify id token + idToken, err := authSvc.ExtractAndVerifyIDToken(ctx, token) + c.Assert(err, qt.IsNil) + c.Assert(idToken, qt.IsNotNil) + + // Test subject set + c.Assert(idToken.Subject, qt.Equals, u.Id) + + // Retrieve the email + email, err := authSvc.Email(idToken) c.Assert(err, qt.IsNil) c.Assert(email, qt.Equals, u.Email) diff --git a/internal/jimm/admin.go b/internal/jimm/admin.go index 60d2a69f7..d39f97b1a 100644 --- a/internal/jimm/admin.go +++ b/internal/jimm/admin.go @@ -39,7 +39,12 @@ func GetDeviceSessionToken(ctx context.Context, authenticator OAuthAuthenticator return "", errors.E(op, err) } - email, err := authenticator.UserInfo(ctx, token) + idToken, err := authenticator.ExtractAndVerifyIDToken(ctx, token) + if err != nil { + return "", errors.E(op, err) + } + + email, err := authenticator.Email(idToken) if err != nil { return "", errors.E(op, err) } diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index 20d1fc79f..fbf22e097 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/coreos/go-oidc/v3/oidc" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/juju/api/base" "github.com/juju/juju/core/crossmodel" @@ -136,9 +137,12 @@ type OAuthAuthenticator interface { // See Device(...) godoc for more info pertaining to the flow. DeviceAccessToken(ctx context.Context, res *oauth2.DeviceAuthResponse) (*oauth2.Token, error) - // UserInfo calls the user info endpoint using the provided token and returns - // user's email. - UserInfo(ctx context.Context, oauth2Token *oauth2.Token) (string, error) + // ExtractAndVerifyIDToken extracts the id token from the extras claims of an oauth2 token + // and performs signature verification of the token. + ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) + + // Email retrieves the users email from an id token via the email claim + Email(idToken *oidc.IDToken) (string, error) // MintSessionToken mints a session token to be used when logging into JIMM // via an access token. The token only contains the user's email for authentication. diff --git a/internal/jimmhttp/auth_handler.go b/internal/jimmhttp/auth_handler.go index cfaed76d0..c23b22dce 100644 --- a/internal/jimmhttp/auth_handler.go +++ b/internal/jimmhttp/auth_handler.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" + "github.com/coreos/go-oidc/v3/oidc" "github.com/go-chi/chi/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" @@ -49,7 +50,8 @@ type OAuthHandlerParams struct { type BrowserOAuthAuthenticator interface { AuthCodeURL() string Exchange(ctx context.Context, code string) (*oauth2.Token, error) - UserInfo(ctx context.Context, oauth2Token *oauth2.Token) (string, error) + ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) + Email(idToken *oidc.IDToken) (string, error) UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error CreateBrowserSession( ctx context.Context, @@ -117,9 +119,15 @@ func (oah *OAuthHandler) Callback(w http.ResponseWriter, r *http.Request) { return } - email, err := authSvc.UserInfo(ctx, token) + idToken, err := authSvc.ExtractAndVerifyIDToken(ctx, token) if err != nil { - writeError(ctx, w, http.StatusBadRequest, err, "failed to retrieve user info") + writeError(ctx, w, http.StatusBadRequest, err, "failed to extract and verify id token") + return + } + + email, err := authSvc.Email(idToken) + if err != nil { + writeError(ctx, w, http.StatusBadRequest, err, "failed to extract email from id token") return } diff --git a/internal/rpc/proxy_test.go b/internal/rpc/proxy_test.go index 79430bbf8..eb5f1aff9 100644 --- a/internal/rpc/proxy_test.go +++ b/internal/rpc/proxy_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" @@ -320,7 +321,14 @@ func (m *mockOAuthAuthenticator) DeviceAccessToken(ctx context.Context, res *oau return &oauth2.Token{}, nil } -func (m *mockOAuthAuthenticator) UserInfo(ctx context.Context, oauth2Token *oauth2.Token) (string, error) { +func (m *mockOAuthAuthenticator) ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) { + if m.err != nil { + return nil, m.err + } + return &oidc.IDToken{}, nil +} + +func (m *mockOAuthAuthenticator) Email(idToken *oidc.IDToken) (string, error) { if m.err != nil { return "", m.err } From b3aeb5f100d97d1a74d25ba3ff85290f3367a05c Mon Sep 17 00:00:00 2001 From: Ales Stimec Date: Fri, 26 Apr 2024 16:13:52 +0300 Subject: [PATCH 110/126] CSS-8226 Vault k8s relation (#1197) * Changes Vault auth. - switches to the new vault relation interface used by the vault-k8s charm - switches vault auth to role_id, role_secret_id Note: did not change the VM charm. also means we won't be able to relate to the vault VM charm. * Update charmcraft.yaml to use pydantic-core binary * Update charm blocked on Vault logic and charm tests - Also updated the ingress relation to the latest and it seems v1 of the relation interface is deprecated, we should move to v2 in a separate PR. * Removed unused functions from charm --------- Co-authored-by: Kian Parvin --- .air.toml | 4 +- .github/workflows/ci.yaml | 2 + Makefile | 4 +- charms/jimm-k8s/charmcraft.yaml | 17 +- charms/jimm-k8s/config.yaml | 8 +- .../lib/charms/traefik_k8s/v1/ingress.py | 143 ++--- .../lib/charms/vault_k8s/v0/vault_kv.py | 595 ++++++++++++++++++ charms/jimm-k8s/metadata.yaml | 1 + charms/jimm-k8s/requirements.txt | 3 +- charms/jimm-k8s/src/charm.py | 223 +++---- charms/jimm-k8s/tests/unit/test_charm.py | 186 +++--- cmd/jimmsrv/main.go | 4 +- docker-compose.yaml | 4 +- go.mod | 22 +- go.sum | 56 +- internal/jimm/controller_test.go | 10 +- internal/jimmjwx/utils_test.go | 19 +- internal/jimmtest/vault.go | 23 +- internal/vault/vault.go | 34 +- internal/vault/vault_test.go | 10 +- internal/wellknownapi/api_test.go | 10 +- local/vault/init.sh | 5 + service.go | 49 +- service_test.go | 14 +- 24 files changed, 992 insertions(+), 454 deletions(-) create mode 100644 charms/jimm-k8s/lib/charms/vault_k8s/v0/vault_kv.py diff --git a/.air.toml b/.air.toml index 6ebe36567..bff0eb5de 100644 --- a/.air.toml +++ b/.air.toml @@ -4,7 +4,7 @@ tmp_dir = "tmp" [build] args_bin = [] - bin = "./tmp/jimm" + bin = "env $(cat /vault/vault.env | xargs) ./tmp/jimm" cmd = "go build -gcflags='all=-N -l' -buildvcs=false -o ./tmp/jimm ./cmd/jimmsrv" delay = 1000 exclude_dir = [".vscode", "assets", "tmp", "vendor", "testdata"] @@ -12,7 +12,7 @@ tmp_dir = "tmp" exclude_regex = ["_test.go"] exclude_unchanged = false follow_symlink = false - full_bin = "dlv exec --accept-multiclient --log --headless --continue --listen :2345 --api-version 2 ./tmp/jimm" + full_bin = "env $(cat /vault/vault.env | xargs) dlv exec --accept-multiclient --log --headless --continue --listen :2345 --api-version 2 ./tmp/jimm" include_dir = [] include_ext = ["go", "tpl", "tmpl", "html"] kill_delay = "0s" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9f6f5e0b8..868ad4c95 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,6 +40,7 @@ jobs: run: | touch ./local/vault/approle.json touch ./local/vault/roleid.txt + touch ./local/vault/vault.env - name: Create test certs run: make certs - name: Start test environment @@ -69,6 +70,7 @@ jobs: run: | touch ./local/vault/approle.json touch ./local/vault/roleid.txt + touch ./local/vault/vault.env - run: go version - run: go mod vendor - run: docker compose --profile dev up -d --wait --timestamps diff --git a/Makefile b/Makefile index 88dd5751f..513cb26a0 100644 --- a/Makefile +++ b/Makefile @@ -33,14 +33,14 @@ certs: @cd local/traefik/certs; ./certs.sh; cd - test-env: sysdeps certs - @touch ./local/vault/approle.json && touch ./local/vault/roleid.txt + @touch ./local/vault/approle.json && touch ./local/vault/roleid.txt && touch ./local/vault/vault.env @docker compose up --force-recreate -d --wait test-env-cleanup: @docker compose down -v --remove-orphans dev-env-setup: sysdeps certs - @touch ./local/vault/approle.json && touch ./local/vault/roleid.txt + @touch ./local/vault/approle.json && touch ./local/vault/roleid.txt && touch ./local/vault/vault.env @make version/commit.txt && make version/version.txt @go mod vendor diff --git a/charms/jimm-k8s/charmcraft.yaml b/charms/jimm-k8s/charmcraft.yaml index 85812501d..299300d5b 100644 --- a/charms/jimm-k8s/charmcraft.yaml +++ b/charms/jimm-k8s/charmcraft.yaml @@ -5,17 +5,18 @@ parts: charm: charm-python-packages: [setuptools] charm-binary-python-packages: - - cryptography - - jsonschema - - PyYAML - - attrs - - importlib-resources - - urllib3 - - zipp + - cryptography + - jsonschema + - PyYAML + - attrs + - importlib-resources + - urllib3 + - zipp + - pydantic-core bases: # This run-on is not as strict as the machine charm # as the jimm-server runs in a container. - # So the only restriction for build-on vs run-on is + # So the only restriction for build-on vs run-on is # the charm code. - build-on: - name: "ubuntu" diff --git a/charms/jimm-k8s/config.yaml b/charms/jimm-k8s/config.yaml index cb3f8223f..43abfae6d 100644 --- a/charms/jimm-k8s/config.yaml +++ b/charms/jimm-k8s/config.yaml @@ -57,12 +57,6 @@ options: dns-name: type: string description: DNS hostname that JIMM is being served from. - vault-access-address: - type: string - description: | - The source address for the connection to Vault. - This should be a single IP with no CIDR. - E.g. 10.1.2.123 jwt-expiry: type: string description: | @@ -92,7 +86,7 @@ options: default: 86400 description: | The max age for the session cookies in seconds, on subsequent logins, the session instance - extended by this amount. + extended by this amount. final-redirect-url: type: string default: "" diff --git a/charms/jimm-k8s/lib/charms/traefik_k8s/v1/ingress.py b/charms/jimm-k8s/lib/charms/traefik_k8s/v1/ingress.py index 69008a73f..b5afac333 100644 --- a/charms/jimm-k8s/lib/charms/traefik_k8s/v1/ingress.py +++ b/charms/jimm-k8s/lib/charms/traefik_k8s/v1/ingress.py @@ -1,54 +1,19 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. -r"""# Interface Library for ingress. +r"""# [DEPRECATED!] Interface Library for ingress. -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. +This is a DEPRECATED version of the Ingress interface library. -## Getting Started +It was dropped in favour of ingress v2 because it contained a data model bug that +could not be fixed while maintaining backwards compatibility. -To get started using the library, you just need to fetch the library using `charmcraft`. +What the bug means, is that by using the ingress v1 interface you are not able to obtain +unit-level load balancing, but instead, all traffic will be routed to your leader unit. +Which is not what you most likely want. -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v1.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") +If it IS what you want after all, consider opening a feature request for explicit +'ingress-per-leader' support. """ import logging @@ -69,7 +34,7 @@ def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 7 +LIBPATCH = 18 DEFAULT_RELATION_NAME = "ingress" RELATION_INTERFACE = "ingress" @@ -98,6 +63,7 @@ def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): "host": {"type": "string"}, "port": {"type": "string"}, "strip-prefix": {"type": "string"}, + "redirect-https": {"type": "string"}, }, "required": ["model", "name", "host", "port"], } @@ -113,12 +79,19 @@ def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): try: from typing import TypedDict except ImportError: - from typing_extensions import TypedDict # py35 compat + from typing_extensions import TypedDict # py35 compatibility # Model of the data a unit implementing the requirer will need to provide. RequirerData = TypedDict( "RequirerData", - {"model": str, "name": str, "host": str, "port": int, "strip-prefix": bool}, + { + "model": str, + "name": str, + "host": str, + "port": int, + "strip-prefix": bool, + "redirect-https": bool, + }, total=False, ) # Provider ingress data model. @@ -135,8 +108,8 @@ def _validate_data(data, schema): if not DO_VALIDATION: return try: - jsonschema.validate(instance=data, schema=schema) - except jsonschema.ValidationError as e: + jsonschema.validate(instance=data, schema=schema) # pyright: ignore[reportUnboundVariable] + except jsonschema.ValidationError as e: # pyright: ignore[reportUnboundVariable] raise DataValidationError(data, schema) from e @@ -148,7 +121,7 @@ class _IngressPerAppBase(Object): """Base class for IngressPerUnit interface classes.""" def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) + super().__init__(charm, relation_name + "_V1") self.charm: CharmBase = charm self.relation_name = relation_name @@ -183,8 +156,8 @@ def _handle_upgrade_or_leader(self, event): class _IPAEvent(RelationEvent): - __args__ = () # type: Tuple[str, ...] - __optional_kwargs__ = {} # type: Dict[str, Any] + __args__: Tuple[str, ...] = () + __optional_kwargs__: Dict[str, Any] = {} @classmethod def __attrs__(cls): @@ -202,7 +175,7 @@ def __init__(self, handle, relation, *args, **kwargs): obj = kwargs.get(attr, default) setattr(self, attr, obj) - def snapshot(self) -> dict: + def snapshot(self): dct = super().snapshot() for attr in self.__attrs__(): obj = getattr(self, attr) @@ -217,7 +190,7 @@ def snapshot(self) -> dict: return dct - def restore(self, snapshot: dict) -> None: + def restore(self, snapshot) -> None: super().restore(snapshot) for attr, obj in snapshot.items(): setattr(self, attr, obj) @@ -226,14 +199,15 @@ def restore(self, snapshot: dict) -> None: class IngressPerAppDataProvidedEvent(_IPAEvent): """Event representing that ingress data has been provided for an app.""" - __args__ = ("name", "model", "port", "host", "strip_prefix") + __args__ = ("name", "model", "port", "host", "strip_prefix", "redirect_https") if typing.TYPE_CHECKING: - name = None # type: Optional[str] - model = None # type: Optional[str] - port = None # type: Optional[str] - host = None # type: Optional[str] - strip_prefix = False # type: bool + name: Optional[str] = None + model: Optional[str] = None + port: Optional[str] = None + host: Optional[str] = None + strip_prefix: bool = False + redirect_https: bool = False class IngressPerAppDataRemovedEvent(RelationEvent): @@ -250,7 +224,7 @@ class IngressPerAppProviderEvents(ObjectEvents): class IngressPerAppProvider(_IngressPerAppBase): """Implementation of the provider of ingress.""" - on = IngressPerAppProviderEvents() + on = IngressPerAppProviderEvents() # type: ignore def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): """Constructor for IngressPerAppProvider. @@ -274,6 +248,7 @@ def _handle_relation(self, event): data["port"], data["host"], data.get("strip-prefix", False), + data.get("redirect-https", False), ) def _handle_relation_broken(self, event): @@ -298,23 +273,23 @@ def _get_requirer_data(self, relation: Relation) -> RequirerData: # type: ignor For convenience, we convert 'port' to integer. """ - assert relation.app, "no app in relation (shouldn't happen)" # for type checker - if not all((relation.app, relation.app.name)): + if not relation.app or not relation.app.name: # type: ignore # Handle edge case where remote app name can be missing, e.g., # relation_broken events. # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 return {} databag = relation.data[relation.app] - remote_data = {} # type: Dict[str, Union[int, str]] - for k in ("port", "host", "model", "name", "mode", "strip-prefix"): + remote_data: Dict[str, Union[int, str]] = {} + for k in ("port", "host", "model", "name", "mode", "strip-prefix", "redirect-https"): v = databag.get(k) if v is not None: remote_data[k] = v _validate_data(remote_data, INGRESS_REQUIRES_APP_SCHEMA) remote_data["port"] = int(remote_data["port"]) - remote_data["strip-prefix"] = bool(remote_data.get("strip-prefix", False)) - return remote_data + remote_data["strip-prefix"] = bool(remote_data.get("strip-prefix", "false") == "true") + remote_data["redirect-https"] = bool(remote_data.get("redirect-https", "false") == "true") + return typing.cast(RequirerData, remote_data) def get_data(self, relation: Relation) -> RequirerData: # type: ignore """Fetch the remote app's databag, i.e. the requirer data.""" @@ -328,18 +303,17 @@ def is_ready(self, relation: Optional[Relation] = None): try: return bool(self._get_requirer_data(relation)) except DataValidationError as e: - log.warning("Requirer not ready; validation error encountered: %s" % str(e)) + log.info("Provider not ready; validation error encountered: %s" % str(e)) return False def _provided_url(self, relation: Relation) -> ProviderIngressData: # type: ignore """Fetch and validate this app databag; return the ingress url.""" - assert relation.app, "no app in relation (shouldn't happen)" # for type checker - if not all((relation.app, relation.app.name, self.unit.is_leader())): + if not relation.app or not relation.app.name or not self.unit.is_leader(): # type: ignore # Handle edge case where remote app name can be missing, e.g., # relation_broken events. # Also, only leader units can read own app databags. # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return {} # noqa + return typing.cast(ProviderIngressData, {}) # noqa # fetch the provider's app databag raw_data = relation.data[self.app].get("ingress") @@ -389,7 +363,7 @@ class IngressPerAppReadyEvent(_IPAEvent): __args__ = ("url",) if typing.TYPE_CHECKING: - url = None # type: Optional[str] + url: Optional[str] = None class IngressPerAppRevokedEvent(RelationEvent): @@ -406,8 +380,9 @@ class IngressPerAppRequirerEvents(ObjectEvents): class IngressPerAppRequirer(_IngressPerAppBase): """Implementation of the requirer of the ingress relation.""" - on = IngressPerAppRequirerEvents() - # used to prevent spur1ious urls to be sent out if the event we're currently + on = IngressPerAppRequirerEvents() # type: ignore + + # used to prevent spurious urls to be sent out if the event we're currently # handling is a relation-broken one. _stored = StoredState() @@ -419,6 +394,7 @@ def __init__( host: Optional[str] = None, port: Optional[int] = None, strip_prefix: bool = False, + redirect_https: bool = False, ): """Constructor for IngressRequirer. @@ -434,14 +410,23 @@ def __init__( host: Hostname to be used by the ingress provider to address the requiring application; if unspecified, the default Kubernetes service name will be used. strip_prefix: configure Traefik to strip the path prefix. + redirect_https: redirect incoming requests to the HTTPS. Request Args: port: the port of the service """ + log.warning( + "The ``ingress v1`` library is DEPRECATED in favour of ``ingress v2`` " + "and no longer maintained. This library does NOT in fact implement the " + "``ingress`` interface, but, instead, the ``ingress-per-leader`` one." + "Please bump with ``charmcraft fetch-lib charms.traefik_k8s.v2.ingress``." + ) + super().__init__(charm, relation_name) self.charm: CharmBase = charm self.relation_name = relation_name self._strip_prefix = strip_prefix + self._redirect_https = redirect_https self._stored.set_default(current_url=None) # type: ignore @@ -481,7 +466,7 @@ def is_ready(self): try: return bool(self._get_url_from_relation_data()) except DataValidationError as e: - log.warning("Requirer not ready; validation error encountered: %s" % str(e)) + log.info("Requirer not ready; validation error encountered: %s" % str(e)) return False def _publish_auto_data(self, relation: Relation): @@ -518,6 +503,9 @@ def provide_ingress_requirements(self, *, host: Optional[str] = None, port: int) if self._strip_prefix: data["strip-prefix"] = "true" + if self._redirect_https: + data["redirect-https"] = "true" + _validate_data(data, INGRESS_REQUIRES_APP_SCHEMA) self.relation.data[self.app].update(data) @@ -532,12 +520,11 @@ def _get_url_from_relation_data(self) -> Optional[str]: Returns None if the URL isn't available yet. """ relation = self.relation - if not relation: + if not relation or not relation.app: return None # fetch the provider's app databag try: - assert relation.app, "no app in relation (shouldn't happen)" # for type checker raw = relation.data.get(relation.app, {}).get("ingress") except ModelError as e: log.debug( @@ -559,6 +546,6 @@ def url(self) -> Optional[str]: Returns None if the URL isn't available yet. """ - data = self._stored.current_url or None # type: ignore + data = self._stored.current_url or self._get_url_from_relation_data() # type: ignore assert isinstance(data, (str, type(None))) # for static checker return data diff --git a/charms/jimm-k8s/lib/charms/vault_k8s/v0/vault_kv.py b/charms/jimm-k8s/lib/charms/vault_k8s/v0/vault_kv.py new file mode 100644 index 000000000..39e5ee324 --- /dev/null +++ b/charms/jimm-k8s/lib/charms/vault_k8s/v0/vault_kv.py @@ -0,0 +1,595 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Library for the vault-kv relation. + +This library contains the Requires and Provides classes for handling the vault-kv +interface. + +## Getting Started +From a charm directory, fetch the library using `charmcraft`: + +```shell +charmcraft fetch-lib charms.vault_k8s.v0.vault_kv +``` + +### Requirer charm +The requirer charm is the charm requiring a secret value store. In this example, the requirer charm +is requiring a secret value store. + +```python +import secrets + +from charms.vault_k8s.v0 import vault_kv +from ops.charm import CharmBase, InstallEvent +from ops.main import main +from ops.model import ActiveStatus, BlockedStatus + +NONCE_SECRET_LABEL = "nonce" + + +class ExampleRequirerCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.interface = vault_kv.VaultKvRequires( + self, + "vault-kv", + "my-suffix", + ) + + self.framework.observe(self.on.install, self._on_install) + self.framework.observe(self.interface.on.connected, self._on_connected) + self.framework.observe(self.interface.on.ready, self._on_ready) + self.framework.observe(self.interface.on.gone_away, self._on_gone_away) + self.framework.observe(self.on.update_status, self._on_update_status) + + def _on_install(self, event: InstallEvent): + self.unit.add_secret( + {"nonce": secrets.token_hex(16)}, + label=NONCE_SECRET_LABEL, + description="Nonce for vault-kv relation", + ) + self.unit.status = BlockedStatus("Waiting for vault-kv relation") + + def _on_connected(self, event: vault_kv.VaultKvConnectedEvent): + relation = self.model.get_relation(event.relation_name, event.relation_id) + egress_subnet = str(self.model.get_binding(relation).network.interfaces[0].subnet) + self.interface.request_credentials(relation, egress_subnet, self.get_nonce()) + + def _on_ready(self, event: vault_kv.VaultKvReadyEvent): + relation = self.model.get_relation(event.relation_name, event.relation_id) + if relation is None: + return + vault_url = self.interface.get_vault_url(relation) + ca_certificate = self.interface.get_ca_certificate(relation) + mount = self.interface.get_mount(relation) + + unit_credentials = self.interface.get_unit_credentials(relation) + # unit_credentials is a juju secret id + secret = self.model.get_secret(id=unit_credentials) + secret_content = secret.get_content() + role_id = secret_content["role-id"] + role_secret_id = secret_content["role-secret-id"] + + self._configure(vault_url, ca_certificate, mount, role_id, role_secret_id) + + self.unit.status = ActiveStatus() + + def _on_gone_away(self, event: vault_kv.VaultKvGoneAwayEvent): + self.unit.status = BlockedStatus("Waiting for vault-kv relation") + + def _configure( + self, + vault_url: str, + ca_certificate: str, + mount: str, + role_id: str, + role_secret_id: str, + ): + pass + + def _on_update_status(self, event): + # Check somewhere that egress subnet has not changed i.e. pod has not been rescheduled + # Update status might not be the best place + binding = self.model.get_binding("vault-kv") + if binding is not None: + egress_subnet = str(binding.network.interfaces[0].subnet) + self.interface.request_credentials(event.relation, egress_subnet, self.get_nonce()) + + def get_nonce(self): + secret = self.model.get_secret(label=NONCE_SECRET_LABEL) + nonce = secret.get_content()["nonce"] + return nonce + + +if __name__ == "__main__": + main(ExampleRequirerCharm) +``` + +You can integrate both charms by running: + +```bash +juju integrate +``` +""" + +import json +import logging +from dataclasses import dataclass +from typing import Any, Dict, Iterable, List, Mapping, Optional, Union + +import ops +from interface_tester.schema_base import DataBagSchema # type: ignore[import-untyped] +from pydantic import BaseModel, Field, Json, ValidationError + +logger = logging.getLogger(__name__) + + +# The unique Charmhub library identifier, never change it +LIBID = "591d6d2fb6a54853b4bb53ef16ef603a" + +# Increment this major API version when introducing breaking changes +LIBAPI = 0 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 4 + +PYDEPS = ["pydantic", "pytest-interface-tester"] + + +class VaultKvProviderSchema(BaseModel): + """Provider side of the vault-kv interface.""" + + vault_url: str = Field(description="The URL of the Vault server to connect to.") + mount: str = Field( + description=( + "The KV mount available for the requirer application, " + "respecting the pattern 'charm--'." + ) + ) + ca_certificate: str = Field(description="The CA certificate to use when validating the Vault server's certificate.") + credentials: Json[Mapping[str, str]] = Field( + description=( + "Mapping of unit name and credentials for that unit." + " Credentials are a juju secret containing a 'role-id' and a 'role-secret-id'." + ) + ) + + +class AppVaultKvRequirerSchema(BaseModel): + """App schema of the requirer side of the vault-kv interface.""" + + mount_suffix: str = Field(description="Suffix to append to the mount name to get the KV mount.") + + +class UnitVaultKvRequirerSchema(BaseModel): + """Unit schema of the requirer side of the vault-kv interface.""" + + egress_subnet: str = Field(description="Egress subnet to use, in CIDR notation.") + nonce: str = Field(description="Uniquely identifying value for this unit. `secrets.token_hex(16)` is recommended.") + + +class ProviderSchema(DataBagSchema): + """The schema for the provider side of this interface.""" + + app: VaultKvProviderSchema # type: ignore + + +class RequirerSchema(DataBagSchema): + """The schema for the requirer side of this interface.""" + + app: AppVaultKvRequirerSchema # type: ignore + unit: UnitVaultKvRequirerSchema # type: ignore + + +@dataclass +class KVRequest: + """This class represents a kv request from an interface Requirer.""" + + relation_id: int + app_name: str + unit_name: str + mount_suffix: str + egress_subnet: str + nonce: str + + +def is_requirer_data_valid(app_data: Mapping[str, str], unit_data: Mapping[str, str]) -> bool: + """Return whether the requirer data is valid.""" + try: + RequirerSchema( + app=AppVaultKvRequirerSchema(**app_data), + unit=UnitVaultKvRequirerSchema(**unit_data), + ) + return True + except ValidationError as e: + logger.debug("Invalid data: %s", e) + return False + + +def is_provider_data_valid(data: Mapping[str, str]) -> bool: + """Return whether the provider data is valid.""" + try: + ProviderSchema(app=VaultKvProviderSchema(**data)) # type: ignore https://github.com/pydantic/pydantic/issues/8616 + return True + except ValidationError as e: + logger.debug("Invalid data: %s", e) + return False + + +class NewVaultKvClientAttachedEvent(ops.EventBase): + """New vault kv client attached event.""" + + def __init__( + self, + handle: ops.Handle, + relation_id: int, + app_name: str, + unit_name: str, + mount_suffix: str, + egress_subnet: str, + nonce: str, + ): + super().__init__(handle) + self.relation_id = relation_id + self.app_name = app_name + self.unit_name = unit_name + self.mount_suffix = mount_suffix + self.egress_subnet = egress_subnet + self.nonce = nonce + + def snapshot(self) -> dict: + """Return snapshot data that should be persisted.""" + return { + "relation_id": self.relation_id, + "app_name": self.app_name, + "unit_name": self.unit_name, + "mount_suffix": self.mount_suffix, + "egress_subnet": self.egress_subnet, + "nonce": self.nonce, + } + + def restore(self, snapshot: Dict[str, Any]): + """Restore the value state from a given snapshot.""" + super().restore(snapshot) + self.relation_id = snapshot["relation_id"] + self.app_name = snapshot["app_name"] + self.unit_name = snapshot["unit_name"] + self.mount_suffix = snapshot["mount_suffix"] + self.egress_subnet = snapshot["egress_subnet"] + self.nonce = snapshot["nonce"] + + +class VaultKvProviderEvents(ops.ObjectEvents): + """List of events that the Vault Kv provider charm can leverage.""" + + new_vault_kv_client_attached = ops.EventSource(NewVaultKvClientAttachedEvent) + + +class VaultKvProvides(ops.Object): + """Class to be instanciated by the providing side of the relation.""" + + on = VaultKvProviderEvents() # type: ignore + + def __init__( + self, + charm: ops.CharmBase, + relation_name: str, + ) -> None: + super().__init__(charm, relation_name) + self.charm = charm + self.relation_name = relation_name + self.framework.observe( + self.charm.on[relation_name].relation_changed, + self._on_relation_changed, + ) + + def _on_relation_changed(self, event: ops.RelationChangedEvent): + """Handle client changed relation. + + This handler will emit a new_vault_kv_client_attached event for each requiring unit + with valid relation data. + """ + if event.app is None: + logger.debug("No remote application yet") + return + app_data = event.relation.data[event.app] + for unit in event.relation.units: + if not is_requirer_data_valid(app_data, event.relation.data[unit]): + logger.debug("Invalid data from unit %r", unit.name) + continue + self.on.new_vault_kv_client_attached.emit( + relation_id=event.relation.id, + app_name=event.app.name, + unit_name=unit.name, + mount_suffix=event.relation.data[event.app]["mount_suffix"], + egress_subnet=event.relation.data[unit]["egress_subnet"], + nonce=event.relation.data[unit]["nonce"], + ) + + def set_vault_url(self, relation: ops.Relation, vault_url: str): + """Set the vault_url on the relation.""" + if not self.charm.unit.is_leader(): + return + + relation.data[self.charm.app]["vault_url"] = vault_url + + def set_ca_certificate(self, relation: ops.Relation, ca_certificate: str): + """Set the ca_certificate on the relation.""" + if not self.charm.unit.is_leader(): + return + if not relation: + logger.warning("Relation is None") + return + if not relation.active: + logger.warning("Relation is not active") + return + relation.data[self.charm.app]["ca_certificate"] = ca_certificate + + def set_mount(self, relation: ops.Relation, mount: str): + """Set the mount on the relation.""" + if not self.charm.unit.is_leader(): + return + + relation.data[self.charm.app]["mount"] = mount + + def set_unit_credentials(self, relation: ops.Relation, nonce: str, secret: ops.Secret): + """Set the unit credentials on the relation.""" + if not self.charm.unit.is_leader(): + return + + credentials = self.get_credentials(relation) + if secret.id is None: + logger.debug( + "Secret id is None, not updating the relation '%s:%d' for nonce %r", + relation.name, + relation.id, + nonce, + ) + return + credentials[nonce] = secret.id + relation.data[self.charm.app]["credentials"] = json.dumps(credentials, sort_keys=True) + + def remove_unit_credentials(self, relation: ops.Relation, nonce: Union[str, Iterable[str]]): + """Remove nonce(s) from the relation.""" + if not self.charm.unit.is_leader(): + return + + if isinstance(nonce, str): + nonce = [nonce] + + credentials = self.get_credentials(relation) + + for n in nonce: + credentials.pop(n, None) + + relation.data[self.charm.app]["credentials"] = json.dumps(credentials, sort_keys=True) + + def get_credentials(self, relation: ops.Relation) -> dict: + """Get the unit credentials from the relation.""" + return json.loads(relation.data[self.charm.app].get("credentials", "{}")) + + def get_outstanding_kv_requests(self, relation_id: Optional[int] = None) -> List[KVRequest]: + """Get the outstanding requests for the relation.""" + outstanding_requests: List[KVRequest] = [] + kv_requests = self.get_kv_requests(relation_id=relation_id) + for request in kv_requests: + if not self._credentials_issued_for_request(nonce=request.nonce, relation_id=relation_id): + outstanding_requests.append(request) + return outstanding_requests + + def get_kv_requests(self, relation_id: Optional[int] = None) -> List[KVRequest]: + """Get all KV requests for the relation.""" + kv_requests: List[KVRequest] = [] + relations = ( + [relation for relation in self.model.relations[self.relation_name] if relation.id == relation_id] + if relation_id is not None + else self.model.relations.get(self.relation_name, []) + ) + for relation in relations: + assert isinstance(relation.app, ops.Application) + if not relation.active: + continue + app_data = relation.data[relation.app] + for unit in relation.units: + unit_data = relation.data[unit] + if not is_requirer_data_valid(app_data=app_data, unit_data=unit_data): + continue + kv_requests.append( + KVRequest( + relation_id=relation.id, + app_name=relation.app.name, + unit_name=unit.name, + mount_suffix=app_data["mount_suffix"], + egress_subnet=unit_data["egress_subnet"], + nonce=unit_data["nonce"], + ) + ) + return kv_requests + + def _credentials_issued_for_request(self, nonce: str, relation_id: Optional[int]) -> bool: + """Return whether credentials have been issued for the request.""" + relation = self.model.get_relation(self.relation_name, relation_id) + if not relation: + return False + credentials = self.get_credentials(relation) + return credentials.get(nonce) is not None + + +class VaultKvConnectedEvent(ops.EventBase): + """VaultKvConnectedEvent Event.""" + + def __init__( + self, + handle: ops.Handle, + relation_id: int, + relation_name: str, + ): + super().__init__(handle) + self.relation_id = relation_id + self.relation_name = relation_name + + def snapshot(self) -> dict: + """Return snapshot data that should be persisted.""" + return { + "relation_id": self.relation_id, + "relation_name": self.relation_name, + } + + def restore(self, snapshot: Dict[str, Any]): + """Restore the value state from a given snapshot.""" + super().restore(snapshot) + self.relation_id = snapshot["relation_id"] + self.relation_name = snapshot["relation_name"] + + +class VaultKvReadyEvent(ops.EventBase): + """VaultKvReadyEvent Event.""" + + def __init__( + self, + handle: ops.Handle, + relation_id: int, + relation_name: str, + ): + super().__init__(handle) + self.relation_id = relation_id + self.relation_name = relation_name + + def snapshot(self) -> dict: + """Return snapshot data that should be persisted.""" + return { + "relation_id": self.relation_id, + "relation_name": self.relation_name, + } + + def restore(self, snapshot: Dict[str, Any]): + """Restore the value state from a given snapshot.""" + super().restore(snapshot) + self.relation_id = snapshot["relation_id"] + self.relation_name = snapshot["relation_name"] + + +class VaultKvGoneAwayEvent(ops.EventBase): + """VaultKvGoneAwayEvent Event.""" + + pass + + +class VaultKvRequireEvents(ops.ObjectEvents): + """List of events that the Vault Kv requirer charm can leverage.""" + + connected = ops.EventSource(VaultKvConnectedEvent) + ready = ops.EventSource(VaultKvReadyEvent) + gone_away = ops.EventSource(VaultKvGoneAwayEvent) + + +class VaultKvRequires(ops.Object): + """Class to be instanciated by the requiring side of the relation.""" + + on = VaultKvRequireEvents() # type: ignore + + def __init__( + self, + charm: ops.CharmBase, + relation_name: str, + mount_suffix: str, + ) -> None: + super().__init__(charm, relation_name) + self.charm = charm + self.relation_name = relation_name + self.mount_suffix = mount_suffix + self.framework.observe( + self.charm.on[relation_name].relation_joined, + self._on_vault_kv_relation_joined, + ) + self.framework.observe( + self.charm.on[relation_name].relation_changed, + self._on_vault_kv_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_broken, + self._on_vault_kv_relation_broken, + ) + + def _set_unit_nonce(self, relation: ops.Relation, nonce: str): + """Set the nonce on the relation.""" + relation.data[self.charm.unit]["nonce"] = nonce + + def _set_unit_egress_subnet(self, relation: ops.Relation, egress_subnet: str): + """Set the egress_subnet on the relation.""" + relation.data[self.charm.unit]["egress_subnet"] = egress_subnet + + def _on_vault_kv_relation_joined(self, event: ops.RelationJoinedEvent): + """Handle relation joined. + + Set the secret backend in the application databag if we are the leader. + Always update the egress_subnet in the unit databag. + """ + if self.charm.unit.is_leader(): + event.relation.data[self.charm.app]["mount_suffix"] = self.mount_suffix + self.on.connected.emit( + event.relation.id, + event.relation.name, + ) + + def _on_vault_kv_relation_changed(self, event: ops.RelationChangedEvent): + """Handle relation changed.""" + if event.app is None: + logger.debug("No remote application yet") + return + + if ( + is_provider_data_valid(event.relation.data[event.app]) + and self.get_unit_credentials(event.relation) is not None + ): + self.on.ready.emit( + event.relation.id, + event.relation.name, + ) + + def _on_vault_kv_relation_broken(self, event: ops.RelationBrokenEvent): + """Handle relation broken.""" + self.on.gone_away.emit() + + def request_credentials(self, relation: ops.Relation, egress_subnet: str, nonce: str) -> None: + """Request credentials from the vault-kv relation. + + Generated secret ids are tied to the unit egress_subnet, so if the egress_subnet + changes a new secret id must be generated. + + A change in egress_subnet can happen when the pod is rescheduled to a different + node by the underlying substrate without a change from Juju. + """ + self._set_unit_egress_subnet(relation, egress_subnet) + self._set_unit_nonce(relation, nonce) + + def get_vault_url(self, relation: ops.Relation) -> Optional[str]: + """Return the vault_url from the relation.""" + if relation.app is None: + return None + return relation.data[relation.app].get("vault_url") + + def get_ca_certificate(self, relation: ops.Relation) -> Optional[str]: + """Return the ca_certificate from the relation.""" + if relation.app is None: + return None + return relation.data[relation.app].get("ca_certificate") + + def get_mount(self, relation: ops.Relation) -> Optional[str]: + """Return the mount from the relation.""" + if relation.app is None: + return None + return relation.data[relation.app].get("mount") + + def get_unit_credentials(self, relation: ops.Relation) -> Optional[str]: + """Return the unit credentials from the relation. + + Unit credentials are stored in the relation data as a Juju secret id. + """ + nonce = relation.data[self.charm.unit].get("nonce") + if nonce is None or relation.app is None: + return None + return json.loads(relation.data[relation.app].get("credentials", "{}")).get(nonce) diff --git a/charms/jimm-k8s/metadata.yaml b/charms/jimm-k8s/metadata.yaml index 5eef23279..36c054123 100644 --- a/charms/jimm-k8s/metadata.yaml +++ b/charms/jimm-k8s/metadata.yaml @@ -54,6 +54,7 @@ requires: vault: interface: vault-kv optional: true + limit: 1 log-proxy: interface: loki_push_api optional: true diff --git a/charms/jimm-k8s/requirements.txt b/charms/jimm-k8s/requirements.txt index 4d6a6257a..2cfd4584a 100644 --- a/charms/jimm-k8s/requirements.txt +++ b/charms/jimm-k8s/requirements.txt @@ -1,8 +1,9 @@ Jinja2 >= 2.11.3 -ops >= 2.0.0 +ops >= 2.12.0 charmhelpers >= 0.20.22 jsonschema >= 3.2.0 cryptography >= 3.4.8 hvac >= 0.11.0 requests >= 2.25.1 jsonschema +pytest-interface-tester \ No newline at end of file diff --git a/charms/jimm-k8s/src/charm.py b/charms/jimm-k8s/src/charm.py index 6d071ce2e..5a755baae 100755 --- a/charms/jimm-k8s/src/charm.py +++ b/charms/jimm-k8s/src/charm.py @@ -14,14 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - -import hashlib import json import logging -import socket +import secrets from urllib.parse import urljoin -import hvac import requests from charms.data_platform_libs.v0.database_requires import ( DatabaseEvent, @@ -46,9 +43,16 @@ IngressPerAppRequirer, IngressPerAppRevokedEvent, ) -from ops.charm import ActionEvent, CharmBase, RelationJoinedEvent +from charms.vault_k8s.v0 import vault_kv +from ops.charm import ActionEvent, CharmBase, InstallEvent, RelationJoinedEvent from ops.main import main -from ops.model import ActiveStatus, BlockedStatus, ErrorStatus, WaitingStatus +from ops.model import ( + ActiveStatus, + BlockedStatus, + ErrorStatus, + TooManyRelatedAppsError, + WaitingStatus, +) from state import State, requires_state, requires_state_setter @@ -79,6 +83,7 @@ OAUTH_SCOPES = "openid email offline_access" # TODO: Add "device_code" below once the charm interface supports it. OAUTH_GRANT_TYPES = ["authorization_code", "refresh_token"] +VAULT_NONCE_SECRET_LABEL = "nonce" class DeferError(Exception): @@ -95,7 +100,6 @@ def __init__(self, *args): super().__init__(*args) self._state = State(self.app, lambda: self.model.get_relation("peer")) - self._unit_state = State(self.unit, lambda: self.model.get_relation("peer")) self.oauth = OAuthRequirer(self, self._oauth_client_config, relation_name=OAUTH) self.framework.observe(self.oauth.on.oauth_info_changed, self._on_oauth_info_changed) @@ -170,9 +174,15 @@ def __init__(self, *args): ) # Vault relation - self.framework.observe(self.on.vault_relation_joined, self._on_vault_relation_joined) - self.framework.observe(self.on.vault_relation_changed, self._on_vault_relation_changed) - self.framework.observe(self.on.vault_relation_departed, self._on_vault_relation_departed) + self.vault = vault_kv.VaultKvRequires( + self, + "vault", + "jimm", + ) + self.framework.observe(self.on.install, self._on_install) + self.framework.observe(self.vault.on.connected, self._on_vault_connected) + self.framework.observe(self.vault.on.ready, self._on_vault_ready) + self.framework.observe(self.vault.on.gone_away, self._on_vault_gone_away) # Grafana relation self._grafana_dashboards = GrafanaDashboardProvider(self, relation_name="grafana-dashboard") @@ -194,10 +204,6 @@ def __init__(self, *args): self._on_create_authorization_model_action, ) - self._local_vault_secret_filename = "vault_secret.js" - self._vault_secret_filename = "/root/config/vault_secret.json" - self._vault_path = "charm-jimm-k8s-creds" - def _on_peer_relation_changed(self, event): self._update_workload(event) @@ -210,6 +216,13 @@ def _on_config_changed(self, event): def _on_oauth_info_changed(self, event: OAuthInfoChangedEvent): self._update_workload(event) + def _on_install(self, event: InstallEvent): + self.unit.add_secret( + {"nonce": secrets.token_hex(16)}, + label=VAULT_NONCE_SECRET_LABEL, + description="Nonce for vault-kv relation", + ) + @requires_state_setter def _on_leader_elected(self, event): if not self._state.private_key: @@ -218,9 +231,37 @@ def _on_leader_elected(self, event): self._update_workload(event) + def _vault_config(self): + try: + relation = self.model.get_relation("vault") + except TooManyRelatedAppsError: + raise RuntimeError("More than one relations are defined. Please provide a relation_id") + if relation is None: + return None + vault_url = self.vault.get_vault_url(relation) + ca_certificate = self.vault.get_ca_certificate(relation) + mount = self.vault.get_mount(relation) + unit_credentials = self.vault.get_unit_credentials(relation) + if not unit_credentials: + return None + + # unit_credentials is a juju secret id + secret = self.model.get_secret(id=unit_credentials) + secret_content = secret.get_content() + role_id = secret_content["role-id"] + role_secret_id = secret_content["role-secret-id"] + + return { + "VAULT_ADDR": vault_url, + "VAULT_CACERT_BYTES": ca_certificate, + "VAULT_ROLE_ID": role_id, + "VAULT_ROLE_SECRET_ID": role_secret_id, + "VAULT_PATH": mount, + } + @requires_state def _update_workload(self, event): - """' Update workload with all available configuration + """Update workload with all available configuration data.""" container = self.unit.get_container(WORKLOAD_CONTAINER) @@ -230,12 +271,6 @@ def _update_workload(self, event): return self.oauth.update_client_config(client_config=self._oauth_client_config) - self._ensure_vault_file(event) - if self.model.get_relation("vault") and not container.exists(self._vault_secret_filename): - logger.warning("Vault relation present but vault setup is not ready yet") - self.unit.status = BlockedStatus("Vault relation present but vault setup is not ready yet") - return - if not self.oauth.is_client_created(): logger.warning("OAuth relation is not ready yet") self.unit.status = BlockedStatus("Waiting for OAuth relation") @@ -278,11 +313,14 @@ def _update_workload(self, event): if self._state.dsn: config_values["JIMM_DSN"] = self._state.dsn - if container.exists(self._vault_secret_filename): - config_values["VAULT_ADDR"] = self._state.vault_address - config_values["VAULT_PATH"] = self._vault_path - config_values["VAULT_SECRET_FILE"] = self._vault_secret_filename - config_values["VAULT_AUTH_PATH"] = "/auth/approle/login" + vault_config = self._vault_config() + insecure_secret_store = self.config.get("postgres-secret-storage", False) + if not vault_config and not insecure_secret_store: + logger.warning("Vault relation is not ready yet") + self.unit.status = BlockedStatus("Waiting for Vault relation") + return + elif vault_config and not insecure_secret_store: + config_values.update(vault_config) if self.model.unit.is_leader(): config_values["JIMM_WATCH_CONTROLLERS"] = "1" @@ -361,18 +399,25 @@ def _on_stop(self, _): logger.info("workload not ready") return - def _on_update_status(self, _): + def _on_update_status(self, event): """Update the status of the charm.""" if self.unit.status.name == ErrorStatus.name: # Skip ready check if unit in error to allow for error resolution. logger.info("unit in error status, skipping ready check") return + try: self._ready() except DeferError: logger.info("workload not ready") return + # update vault relation if exists + binding = self.model.get_binding("vault-kv") + if binding is not None: + egress_subnet = str(binding.network.interfaces[0].subnet) + self.interface.request_credentials(event.relation, egress_subnet, self.get_vault_nonce()) + @requires_state_setter def _on_dashboard_relation_joined(self, event: RelationJoinedEvent): dns_name = self._get_dns_name(event) @@ -452,96 +497,15 @@ def _ready(self): def _get_network_address(self, event): return str(self.model.get_binding(event.relation).network.egress_subnets[0].network_address) - def _on_vault_relation_joined(self, event): - if self.config.get("vault-access-address") is None: - logger.error("Missing config vault-access-address for vault relation") - raise ValueError("Missing config vault-access-address for vault relation") + def _on_vault_connected(self, event: vault_kv.VaultKvConnectedEvent): + relation = self.model.get_relation(event.relation_name, event.relation_id) + egress_subnet = str(self.model.get_binding(relation).network.interfaces[0].subnet) + self.vault.request_credentials(relation, egress_subnet, self.get_vault_nonce()) - event.relation.data[self.unit]["secret_backend"] = json.dumps(self._vault_path) - event.relation.data[self.unit]["hostname"] = json.dumps(socket.gethostname()) - event.relation.data[self.unit]["access_address"] = self.config["vault-access-address"] - event.relation.data[self.unit]["isolated"] = json.dumps(False) - - def _ensure_vault_file(self, event): - container = self.unit.get_container(WORKLOAD_CONTAINER) - - if not self._unit_state.is_ready(): - logger.info("unit state not ready") - event.defer() - return - - # if we can't connect to the container we should defer - # this event. - if not container.can_connect(): - event.defer() - return - - if container.exists(self._vault_secret_filename): - container.remove_path(self._vault_secret_filename) - - secret_data = self._unit_state.vault_secret_data - if secret_data: - self._push_to_workload(self._vault_secret_filename, secret_data, event) - - def _on_vault_relation_departed(self, event): - if self._unit_state.vault_secret_data is not None: - logger.info("secret data found will remove") - self._unit_state.vault_secret_data = None - - container = self.unit.get_container(WORKLOAD_CONTAINER) - if not container.can_connect(): - logger.info("cannot connect to the workload container - deferring the event") - event.defer() - return - - if container.exists(self._vault_secret_filename): - logger.info("Removing vault secret from workload container") - container.remove_path(self._vault_secret_filename) - - def _on_vault_relation_changed(self, event): - if not self._unit_state.is_ready() or not self._state.is_ready(): - logger.info("state not ready") - event.defer() - return - - if self._unit_state.vault_secret_data is not None: - return - - addr = None - role_id = None - token = None - try: - for key, value in event.relation.data[event.unit].items(): - value = value.strip('"') - if "vault_url" in key: - addr = value - if "_role_id" in key: - role_id = value - if "_token" in key: - token = value - except Exception: - logger.warning("Vault relation not ready") - return - if not addr: - logger.warning("Vault address not received") - return - if not role_id: - logger.warning("Vault roleid not received") - return - if not token: - logger.warning("Vault token not received") - return - client = hvac.Client(url=addr, token=token) - secret = client.sys.unwrap() - secret["data"]["role_id"] = role_id - - secret_data = json.dumps(secret) - - logger.info("setting unit state data {}".format(secret_data)) - self._unit_state.vault_secret_data = secret_data - if self.unit.is_leader(): - self._state.vault_address = addr + def _on_vault_ready(self, event: vault_kv.VaultKvReadyEvent): + self._update_workload(event) + def _on_vault_gone_away(self, event: vault_kv.VaultKvGoneAwayEvent): self._update_workload(event) def _path_exists_in_workload(self, path: str): @@ -552,30 +516,6 @@ def _path_exists_in_workload(self, path: str): return container.exists(path) return False - def _push_to_workload(self, filename, content, event): - """Create file on the workload container with - the specified content.""" - - container = self.unit.get_container(WORKLOAD_CONTAINER) - if container.can_connect(): - logger.info("pushing file {} to the workload containe".format(filename)) - container.push(filename, content, make_dirs=True) - else: - logger.info("workload container not ready - defering") - event.defer() - - def _hash(self, filename): - buffer_size = 65536 - md5 = hashlib.md5() - - with open(filename, "rb") as f: - while True: - data = f.read(buffer_size) - if not data: - break - md5.update(data) - return md5.hexdigest() - @requires_state_setter def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): if not event.store_id: @@ -749,6 +689,11 @@ def _oauth_client_config(self) -> ClientConfig: OAUTH_GRANT_TYPES, ) + def get_vault_nonce(self): + secret = self.model.get_secret(label=VAULT_NONCE_SECRET_LABEL) + nonce = secret.get_content()["nonce"] + return nonce + def ensureFQDN(dns: str): # noqa: N802 """Ensures a domain name has an https:// prefix.""" diff --git a/charms/jimm-k8s/tests/unit/test_charm.py b/charms/jimm-k8s/tests/unit/test_charm.py index 32c2717e0..8ca9018ab 100644 --- a/charms/jimm-k8s/tests/unit/test_charm.py +++ b/charms/jimm-k8s/tests/unit/test_charm.py @@ -8,7 +8,6 @@ import pathlib import tempfile import unittest -from unittest.mock import patch from ops.model import ActiveStatus, BlockedStatus from ops.testing import Harness @@ -39,13 +38,12 @@ "uuid": "1234567890", "public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", - "vault-access-address": "10.0.1.123", "final-redirect-url": "some-url", } -EXPECTED_ENV = { +BASE_ENV = { "JIMM_DASHBOARD_LOCATION": "https://jaas.ai/models", - "JIMM_DNS_NAME": "juju-jimm-k8s-0.juju-jimm-k8s-endpoints.None.svc.cluster.local", + "JIMM_DNS_NAME": "juju-jimm-k8s-0.juju-jimm-k8s-endpoints.jimm-model.svc.cluster.local", "JIMM_ENABLE_JWKS_ROTATOR": "1", "JIMM_LISTEN_ADDR": ":8080", "JIMM_LOG_LEVEL": "info", @@ -66,6 +64,18 @@ "JIMM_SESSION_COOKIE_MAX_AGE:": 86400, } +# The environment may optionally include Vault. +EXPECTED_VAULT_ENV = BASE_ENV.copy() +EXPECTED_VAULT_ENV.update( + { + "VAULT_ADDR": "127.0.0.1:8081", + "VAULT_CACERT_BYTES": "abcd", + "VAULT_PATH": "charm-juju-jimm-k8s-jimm", + "VAULT_ROLE_ID": "111", + "VAULT_ROLE_SECRET_ID": "222", + } +) + def get_expected_plan(env): return { @@ -99,6 +109,7 @@ def setUp(self): self.harness = Harness(JimmOperatorCharm) self.addCleanup(self.harness.cleanup) self.harness.disable_hooks() + self.harness.set_model_name("jimm-model") self.harness.add_oci_resource("jimm-image") self.harness.set_can_connect("jimm", True) self.harness.set_leader(True) @@ -115,6 +126,48 @@ def setUp(self): self.ingress_rel_id = self.harness.add_relation("ingress", "nginx-ingress") self.harness.add_relation_unit(self.ingress_rel_id, "nginx-ingress/0") + self.add_oauth_relation() + + def add_openfga_relation(self): + self.openfga_rel_id = self.harness.add_relation("openfga", "openfga") + self.harness.add_relation_unit(self.openfga_rel_id, "openfga/0") + self.harness.update_relation_data( + self.openfga_rel_id, + "openfga", + { + **OPENFGA_PROVIDER_INFO, + }, + ) + + def add_vault_relation(self): + self.harness.charm.on.install.emit() + id = self.harness.add_relation("vault", "vault-k8s") + self.harness.add_relation_unit(id, "vault-k8s/0") + + data = self.harness.get_relation_data(id, "juju-jimm-k8s/0") + self.assertTrue(data) + self.assertTrue("egress_subnet" in data) + self.assertTrue("nonce" in data) + + secret_id = self.harness.add_model_secret( + "vault-k8s/0", + {"role-id": "111", "role-secret-id": "222"}, + ) + self.harness.grant_secret(secret_id, "juju-jimm-k8s") + + credentials = {data["nonce"]: secret_id} + self.harness.update_relation_data( + id, + "vault-k8s", + { + "vault_url": "127.0.0.1:8081", + "ca_certificate": "abcd", + "mount": "charm-juju-jimm-k8s-jimm", + "credentials": json.dumps(credentials, sort_keys=True), + }, + ) + + def add_oauth_relation(self): self.oauth_rel_id = self.harness.add_relation("oauth", "hydra") self.harness.add_relation_unit(self.oauth_rel_id, "hydra/0") secret_id = self.harness.add_model_secret("hydra", {"secret": OAUTH_CLIENT_SECRET}) @@ -129,19 +182,10 @@ def setUp(self): }, ) - def add_openfga_relation(self): - self.openfga_rel_id = self.harness.add_relation("openfga", "openfga") - self.harness.add_relation_unit(self.openfga_rel_id, "openfga/0") - self.harness.update_relation_data( - self.openfga_rel_id, - "openfga", - { - **OPENFGA_PROVIDER_INFO, - }, - ) - # import ipdb; ipdb.set_trace() def test_on_pebble_ready(self): + self.harness.enable_hooks() + self.add_vault_relation() self.harness.update_config(MINIMAL_CONFIG) container = self.harness.model.unit.get_container("jimm") @@ -150,9 +194,11 @@ def test_on_pebble_ready(self): # Check the that the plan was updated plan = self.harness.get_container_pebble_plan("jimm") - self.assertEqual(plan.to_dict(), get_expected_plan(EXPECTED_ENV)) + self.assertEqual(plan.to_dict(), get_expected_plan(EXPECTED_VAULT_ENV)) def test_on_config_changed(self): + self.harness.enable_hooks() + self.add_vault_relation() container = self.harness.model.unit.get_container("jimm") self.harness.charm.on.jimm_pebble_ready.emit(container) @@ -164,22 +210,16 @@ def test_on_config_changed(self): # Check the that the plan was updated plan = self.harness.get_container_pebble_plan("jimm") - self.assertEqual(plan.to_dict(), get_expected_plan(EXPECTED_ENV)) + self.assertEqual(plan.to_dict(), get_expected_plan(EXPECTED_VAULT_ENV)) def test_postgres_secret_storage_config(self): - container = self.harness.model.unit.get_container("jimm") - self.harness.charm.on.jimm_pebble_ready.emit(container) - self.harness.update_config(MINIMAL_CONFIG) self.harness.update_config({"postgres-secret-storage": True}) - self.harness.set_leader(True) - - # Emit the pebble-ready event for jimm + container = self.harness.model.unit.get_container("jimm") self.harness.charm.on.jimm_pebble_ready.emit(container) - # Check the that the plan was updated plan = self.harness.get_container_pebble_plan("jimm") - expected_env = EXPECTED_ENV.copy() + expected_env = BASE_ENV.copy() expected_env.update({"INSECURE_SECRET_STORAGE": "enabled"}) self.assertEqual(plan.to_dict(), get_expected_plan(expected_env)) @@ -227,6 +267,8 @@ def test_app_enters_block_state_if_oauth_relation_not_ready(self): self.assertEqual(self.harness.charm.unit.status.message, "Waiting for OAuth relation") def test_audit_log_retention_config(self): + self.harness.enable_hooks() + self.add_vault_relation() container = self.harness.model.unit.get_container("jimm") self.harness.charm.on.jimm_pebble_ready.emit(container) @@ -235,7 +277,7 @@ def test_audit_log_retention_config(self): # Emit the pebble-ready event for jimm self.harness.charm.on.jimm_pebble_ready.emit(container) - expected_env = EXPECTED_ENV.copy() + expected_env = EXPECTED_VAULT_ENV.copy() expected_env.update({"JIMM_AUDIT_LOG_RETENTION_PERIOD_IN_DAYS": "10"}) # Check the that the plan was updated plan = self.harness.get_container_pebble_plan("jimm") @@ -267,94 +309,13 @@ def test_dashboard_relation_joined(self): ) self.assertEqual(data["is_juju"], "False") - @patch("socket.gethostname") - @patch("hvac.Client.sys") - def test_vault_relation_joined(self, hvac_client_sys, gethostname): - gethostname.return_value = "test-hostname" - hvac_client_sys.unwrap.return_value = { - "key1": "value1", - "data": {"key2": "value2"}, - } - - harness = Harness(JimmOperatorCharm) - self.addCleanup(harness.cleanup) - - jimm_id = harness.add_relation("peer", "juju-jimm-k8s") - harness.add_relation_unit(jimm_id, "juju-jimm-k8s/1") - - dashboard_id = harness.add_relation("dashboard", "juju-dashboard") - harness.add_relation_unit(dashboard_id, "juju-dashboard/0") - - harness.update_config( - { - "controller-admins": "user1 user2 group1", - "uuid": "caaa4ba4-e2b5-40dd-9bf3-2bd26d6e17aa", - "vault-access-address": "10.0.1.123", - } - ) - harness.set_leader(True) - harness.begin_with_initial_hooks() - - container = harness.model.unit.get_container("jimm") - # Emit the pebble-ready event for jimm - harness.charm.on.jimm_pebble_ready.emit(container) - - id = harness.add_relation("vault", "vault-k8s") - harness.add_relation_unit(id, "vault-k8s/0") - data = harness.get_relation_data(id, "juju-jimm-k8s/0") - - self.assertTrue(data) - self.assertEqual( - data["secret_backend"], - '"charm-jimm-k8s-creds"', - ) - self.assertEqual(data["hostname"], '"test-hostname"') - self.assertEqual(data["access_address"], "10.0.1.123") - - harness.update_relation_data( - id, - "vault-k8s/0", - { - "vault_url": '"127.0.0.1:8081"', - "juju-jimm-k8s/0_role_id": '"juju-jimm-k8s-0-test-role-id"', - "juju-jimm-k8s/0_token": '"juju-jimm-k8s-0-test-token"', - }, - ) - - vault_data = container.pull("/root/config/vault_secret.json") - vault_json = json.loads(vault_data.read()) - self.assertEqual( - vault_json, - { - "key1": "value1", - "data": { - "key2": "value2", - "role_id": "juju-jimm-k8s-0-test-role-id", - }, - }, - ) + def test_vault_relation_joined(self): + self.harness.enable_hooks() + self.add_vault_relation() - def test_app_enters_blocked_state_if_vault_related_but_not_ready(self): self.harness.update_config(MINIMAL_CONFIG) - container = self.harness.model.unit.get_container("jimm") - # Emit the pebble-ready event for jimm - self.harness.add_relation("vault", "remote-app-name") - self.harness.charm.on.jimm_pebble_ready.emit(container) - - self.assertEqual(self.harness.charm.unit.status.name, BlockedStatus.name) - self.assertEqual( - self.harness.charm.unit.status.message, "Vault relation present but vault setup is not ready yet" - ) - - def test_app_raises_error_without_vault_config(self): - self.harness.enable_hooks() - minim_config_no_vault_config = MINIMAL_CONFIG.copy() - del minim_config_no_vault_config["vault-access-address"] - self.harness.update_config(minim_config_no_vault_config) - id = self.harness.add_relation("vault", "vault") - with self.assertRaises(ValueError) as e: - self.harness.add_relation_unit(id, "vault/0") - self.assertEqual(e, "Missing config vault-access-address for vault relation") + plan = self.harness.get_container_pebble_plan("jimm") + self.assertEqual(plan.to_dict(), get_expected_plan(EXPECTED_VAULT_ENV)) def test_app_blocked_without_private_key(self): self.harness.enable_hooks() @@ -362,6 +323,7 @@ def test_app_blocked_without_private_key(self): self.harness.charm._state.dsn = "postgres-dsn" # Setup the OpenFGA relation. self.add_openfga_relation() + self.add_vault_relation() self.harness.charm._state.openfga_auth_model_id = 1 # Set the config with the private-key value missing. min_config_no_private_key = MINIMAL_CONFIG.copy() diff --git a/cmd/jimmsrv/main.go b/cmd/jimmsrv/main.go index 33d2acd3f..da83d011d 100644 --- a/cmd/jimmsrv/main.go +++ b/cmd/jimmsrv/main.go @@ -138,9 +138,9 @@ func start(ctx context.Context, s *service.Service) error { ControllerUUID: os.Getenv("JIMM_UUID"), DSN: os.Getenv("JIMM_DSN"), ControllerAdmins: strings.Fields(os.Getenv("JIMM_ADMINS")), - VaultSecretFile: os.Getenv("VAULT_SECRET_FILE"), + VaultRoleID: os.Getenv("VAULT_ROLE_ID"), + VaultRoleSecretID: os.Getenv("VAULT_ROLE_SECRET_ID"), VaultAddress: os.Getenv("VAULT_ADDR"), - VaultAuthPath: os.Getenv("VAULT_AUTH_PATH"), VaultPath: os.Getenv("VAULT_PATH"), DashboardLocation: os.Getenv("JIMM_DASHBOARD_LOCATION"), PublicDNSName: os.Getenv("JIMM_DNS_NAME"), diff --git a/docker-compose.yaml b/docker-compose.yaml index 94d441933..086ba25e7 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -50,8 +50,6 @@ services: # Note: You can comment out the Vault ENV vars below and instead use INSECURE_SECRET_STORAGE to place secrets in Postgres. VAULT_ADDR: "http://vault:8200" VAULT_PATH: "/jimm-kv/" - VAULT_SECRET_FILE: "/vault/approle.json" - VAULT_AUTH_PATH: "/auth/approle/login" # Note: By default we should use Vault as that is the primary means of secret storage. # INSECURE_SECRET_STORAGE: "enabled" # JIMM_DASHBOARD_LOCATION: "" @@ -83,6 +81,7 @@ services: - ./:/jimm/ - ./local/vault/approle.json:/vault/approle.json:rw - ./local/vault/roleid.txt:/vault/roleid.txt:rw + - ./local/vault/vault.env:/vault/vault.env:rw healthcheck: test: [ "CMD", "curl", "http://jimm.localhost:80" ] interval: 5s @@ -146,6 +145,7 @@ services: - ./local/vault/policy.hcl:/vault/policy.hcl - ./local/vault/approle.json:/vault/approle.json - ./local/vault/roleid.txt:/vault/roleid.txt:rw + - ./local/vault/vault.env:/vault/vault.env:rw command: /vault/init.sh depends_on: db: diff --git a/go.mod b/go.mod index 1a650eb26..ade08dba0 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/google/uuid v1.5.0 github.com/gorilla/websocket v1.5.1 github.com/gosuri/uitable v0.0.4 - github.com/hashicorp/vault/api v1.10.0 + github.com/hashicorp/vault/api v1.13.0 github.com/jackc/pgconn v1.13.0 github.com/jackc/pgx/v4 v4.17.2 github.com/juju/cmd/v3 v3.0.14 @@ -33,7 +33,7 @@ require ( github.com/prometheus/client_golang v1.18.0 github.com/rogpeppe/fastuuid v1.2.0 go.uber.org/zap v1.24.0 - golang.org/x/net v0.21.0 // indirect + golang.org/x/net v0.24.0 // indirect golang.org/x/sync v0.5.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/macaroon-bakery.v2 v2.3.0 @@ -52,6 +52,7 @@ require ( github.com/go-chi/render v1.0.2 github.com/gorilla/sessions v1.2.1 github.com/hashicorp/golang-lru/v2 v2.0.7 + github.com/hashicorp/vault/api/auth/approle v0.6.0 github.com/itchyny/gojq v0.12.12 github.com/juju/charm/v12 v12.0.0 github.com/juju/names/v5 v5.0.0 @@ -104,7 +105,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f // indirect github.com/canonical/lxd v0.0.0-20231214113525-e676fc63c50a // indirect - github.com/cenkalti/backoff/v3 v3.0.0 // indirect + github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cjlapao/common-go v0.0.39 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect @@ -115,7 +116,7 @@ require ( github.com/docker/distribution v2.8.3+incompatible // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/fatih/color v1.14.1 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -123,6 +124,7 @@ require ( github.com/gdamore/tcell/v2 v2.5.1 // indirect github.com/go-goose/goose/v5 v5.0.0-20230421180421-abaee9096e3a // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect + github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect @@ -147,11 +149,11 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-retryablehttp v0.6.6 // indirect + github.com/hashicorp/go-retryablehttp v0.7.5 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect - github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect - github.com/hashicorp/go-sockaddr v1.0.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.6 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/im7mortal/kmutex v1.0.1 // indirect github.com/imdario/mergo v0.3.12 // indirect @@ -295,10 +297,10 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.21.0 // indirect + golang.org/x/crypto v0.22.0 // indirect golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/api v0.154.0 // indirect diff --git a/go.sum b/go.sum index c2e7de565..375f90f37 100644 --- a/go.sum +++ b/go.sum @@ -151,8 +151,9 @@ github.com/canonical/lxd v0.0.0-20231214113525-e676fc63c50a h1:Tfo/MzXK5GeG7gzSH github.com/canonical/lxd v0.0.0-20231214113525-e676fc63c50a/go.mod h1:UxfHGKFoRjgu1NUA9EFiR++dKvyAiT0h9HT0ffMlzjc= github.com/canonical/ofga v0.10.0 h1:DHXhG/DAXWWQT/I+2jzr4qm0uTIYrILmtMxd6ZqmEzE= github.com/canonical/ofga v0.10.0/go.mod h1:u4Ou8dbIhO7FmVlT7W3rX2roD9AOGz/CqmGh7AdF0Lo= -github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= +github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -222,8 +223,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= -github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 h1:fmFk0Wt3bBxxwZnu48jqMdaOR/IZ4vdtJFuaFV8MpIE= @@ -255,8 +256,11 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-goose/goose/v5 v5.0.0-20230421180421-abaee9096e3a h1:H/l82+fC6idmYg1kfpQlCq7gYctri7AGn9RemqwN6bw= github.com/go-goose/goose/v5 v5.0.0-20230421180421-abaee9096e3a/go.mod h1:BxICmnmP7QlxZhKP2BHkpWQS0tbb3LrsrLtd9TQyyms= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= +github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -348,6 +352,7 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -411,6 +416,7 @@ github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -418,19 +424,22 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= +github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 h1:iBt4Ew4XEGLfh6/bPk4rSYmuZJGizr6/x/AEizP0CQc= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8/go.mod h1:aiJI+PIApBRQG7FZTEBx5GiiX+HbOHilUdNxUZi4eV0= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I= +github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -446,8 +455,11 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/vault/api v1.10.0 h1:/US7sIjWN6Imp4o/Rj1Ce2Nr5bki/AXi9vAW3p2tOJQ= -github.com/hashicorp/vault/api v1.10.0/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8= +github.com/hashicorp/vault/api v1.12.0/go.mod h1:si+lJCYO7oGkIoNPAN8j3azBLTn9SjMGS+jFaHd1Cck= +github.com/hashicorp/vault/api v1.13.0 h1:RTCGpE2Rgkn9jyPcFlc7YmNocomda44k5ck8FKMH41Y= +github.com/hashicorp/vault/api v1.13.0/go.mod h1:0cb/uZUv1w2cVu9DIvuW1SMlXXC6qtATJt+LXJRx+kg= +github.com/hashicorp/vault/api/auth/approle v0.6.0 h1:ELfFFQlTM/e97WJKu1HvNFa7lQ3tlTwwzrR1NJE1V7Y= +github.com/hashicorp/vault/api/auth/approle v0.6.0/go.mod h1:CCoIl1xBC3lAWpd1HV+0ovk76Z8b8Mdepyk21h3pGk0= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/im7mortal/kmutex v1.0.1 h1:zAACzjwD+OEknDqnLdvRa/BhzFM872EBwKijviGLc9Q= @@ -790,6 +802,7 @@ github.com/mattn/go-colorable v0.0.6/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -801,6 +814,7 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= @@ -1120,9 +1134,11 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1212,8 +1228,9 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1262,6 +1279,7 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1311,9 +1329,11 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1322,9 +1342,11 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1338,11 +1360,13 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/jimm/controller_test.go b/internal/jimm/controller_test.go index d9fc14af7..ec63015ba 100644 --- a/internal/jimm/controller_test.go +++ b/internal/jimm/controller_test.go @@ -191,15 +191,15 @@ func TestAddController(t *testing.T) { func TestAddControllerWithVault(t *testing.T) { c := qt.New(t) - client, path, creds, ok := jimmtest.VaultClient(c, "../../") + client, path, roleID, roleSecretID, ok := jimmtest.VaultClient(c, "../../") if !ok { c.Skip("vault not available") } store := &vault.VaultStore{ - Client: client, - AuthSecret: creds, - AuthPath: "/auth/approle/login", - KVPath: path, + Client: client, + RoleID: roleID, + RoleSecretID: roleSecretID, + KVPath: path, } now := time.Now().UTC().Round(time.Millisecond) diff --git a/internal/jimmjwx/utils_test.go b/internal/jimmjwx/utils_test.go index 9eb63bb82..62c3e73dd 100644 --- a/internal/jimmjwx/utils_test.go +++ b/internal/jimmjwx/utils_test.go @@ -18,16 +18,15 @@ import ( ) func newStore(t testing.TB) *vault.VaultStore { - client, path, creds, ok := jimmtest.VaultClient(t, "../../") - + client, path, roleID, roleSecretID, ok := jimmtest.VaultClient(t, "../../") if !ok { t.Skip("vault not available") } return &vault.VaultStore{ - Client: client, - AuthSecret: creds, - AuthPath: "/auth/approle/login", - KVPath: path, + Client: client, + RoleID: roleID, + RoleSecretID: roleSecretID, + KVPath: path, } } @@ -92,11 +91,15 @@ func setupService(ctx context.Context, c *qt.C) (*jimm.Service, *httptest.Server _, _, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) + _, path, roleID, roleSecretID, ok := jimmtest.VaultClient(c, "../../") + c.Assert(ok, qt.IsTrue) + p := jimmtest.NewTestJimmParams(c) p.VaultAddress = "http://localhost:8200" p.VaultAuthPath = "/auth/approle/login" - p.VaultPath = "/jimm-kv/" - p.VaultSecretFile = "../../local/vault/approle.json" + p.VaultPath = path + p.VaultRoleID = roleID + p.VaultRoleSecretID = roleSecretID p.OpenFGAParams = jimm.OpenFGAParams{ Scheme: cofgaParams.Scheme, Host: cofgaParams.Host, diff --git a/internal/jimmtest/vault.go b/internal/jimmtest/vault.go index 5063e0a85..aefdf73b5 100644 --- a/internal/jimmtest/vault.go +++ b/internal/jimmtest/vault.go @@ -16,7 +16,7 @@ type fatalF interface { } // VaultClient returns a new vault client for use in a test. -func VaultClient(tb fatalF, prefix string) (*api.Client, string, map[string]interface{}, bool) { +func VaultClient(tb fatalF, prefix string) (*api.Client, string, string, string, bool) { cfg := api.DefaultConfig() cfg.Address = "http://localhost:8200" vaultClient, _ := api.NewClient(cfg) @@ -27,14 +27,27 @@ func VaultClient(tb fatalF, prefix string) (*api.Client, string, map[string]inte panic("cannot read " + path.Join(prefix, "./local/vault/approle.json") + " " + wd) } - creds := make(map[string]interface{}) var vaultAPISecret api.Secret err = json.Unmarshal(b, &vaultAPISecret) if err != nil { panic("cannot unmarshal vault secret") } - creds["role_id"] = vaultAPISecret.Data["role_id"] - creds["secret_id"] = vaultAPISecret.Data["secret_id"] - return vaultClient, "/jimm-kv/", creds, true + roleID, ok := vaultAPISecret.Data["role_id"] + if !ok { + panic("role ID not found") + } + roleSecretID, ok := vaultAPISecret.Data["secret_id"] + if !ok { + panic("role secret ID not found") + } + roleIDString, ok := roleID.(string) + if !ok { + panic("failed to convert role ID to string") + } + roleSecretIDString, ok := roleSecretID.(string) + if !ok { + panic("failed to convert role secret ID to string") + } + return vaultClient, "/jimm-kv/", roleIDString, roleSecretIDString, true } diff --git a/internal/vault/vault.go b/internal/vault/vault.go index a25809963..e8618bae2 100644 --- a/internal/vault/vault.go +++ b/internal/vault/vault.go @@ -12,6 +12,7 @@ import ( "time" "github.com/hashicorp/vault/api" + auth "github.com/hashicorp/vault/api/auth/approle" "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "github.com/lestrrat-go/jwx/v2/jwk" @@ -31,13 +32,11 @@ type VaultStore struct { // service. This client is not modified by the store. Client *api.Client - // AuthSecret contains the secret used to authenticate with the - // vault service. - AuthSecret map[string]interface{} + // RoleID is the AppRole role ID. + RoleID string - // AuthPath is the path of the endpoint used to authenticate with - // the vault service. - AuthPath string + // RoleSecretID is the AppRole secret ID. + RoleSecretID string // KVPath is the root path in the vault for JIMM's key-value // storage. @@ -499,15 +498,30 @@ func (s *VaultStore) client(ctx context.Context) (*api.Client, error) { return s.client_, nil } - secret, err := s.Client.Logical().WriteWithContext(ctx, s.AuthPath, s.AuthSecret) + roleSecretID := &auth.SecretID{ + FromString: s.RoleSecretID, + } + appRoleAuth, err := auth.NewAppRoleAuth( + s.RoleID, + roleSecretID, + ) if err != nil { - return nil, errors.E(op, err) + return nil, errors.E(op, err, "unable to initialize approle auth method") + } + + authInfo, err := s.Client.Auth().Login(ctx, appRoleAuth) + if err != nil { + return nil, errors.E(op, err, "unable to login to approle auth method") } - ttl, err := secret.TokenTTL() + if authInfo == nil { + return nil, errors.E(op, "no auth info was returned after login") + } + + ttl, err := authInfo.TokenTTL() if err != nil { return nil, errors.E(op, err) } - tok, err := secret.TokenID() + tok, err := authInfo.TokenID() if err != nil { return nil, errors.E(op, err) } diff --git a/internal/vault/vault_test.go b/internal/vault/vault_test.go index 5a2eaa3bd..3d6032e2a 100644 --- a/internal/vault/vault_test.go +++ b/internal/vault/vault_test.go @@ -28,15 +28,15 @@ func TestMain(m *testing.M) { } func newStore(t testing.TB) *vault.VaultStore { - client, path, creds, ok := jimmtest.VaultClient(t, "../../") + client, path, roleID, secretID, ok := jimmtest.VaultClient(t, "../../") if !ok { t.Skip("vault not available") } return &vault.VaultStore{ - Client: client, - AuthSecret: creds, - AuthPath: "/auth/approle/login", - KVPath: path, + Client: client, + RoleID: roleID, + RoleSecretID: secretID, + KVPath: path, } } diff --git a/internal/wellknownapi/api_test.go b/internal/wellknownapi/api_test.go index 1cba82130..8f809d5b2 100644 --- a/internal/wellknownapi/api_test.go +++ b/internal/wellknownapi/api_test.go @@ -20,15 +20,15 @@ import ( ) func newStore(t testing.TB) *vault.VaultStore { - client, path, creds, ok := jimmtest.VaultClient(t, "../../") + client, path, roleID, roleSecretID, ok := jimmtest.VaultClient(t, "../../") if !ok { t.Skip("vault not available") } return &vault.VaultStore{ - Client: client, - AuthSecret: creds, - AuthPath: "/auth/approle/login", - KVPath: path, + Client: client, + RoleID: roleID, + RoleSecretID: roleSecretID, + KVPath: path, } } diff --git a/local/vault/init.sh b/local/vault/init.sh index f49bc94e0..2ff44ad22 100755 --- a/local/vault/init.sh +++ b/local/vault/init.sh @@ -52,4 +52,9 @@ VAULT_TOKEN=$JIMM_SECRET_WRAPPED vault unwrap > /vault/approle_tmp.yaml echo "$JIMM_ROLE_ID" > /vault/roleid.txt jq ".data.role_id = \"$JIMM_ROLE_ID\"" /vault/approle_tmp.yaml > /vault/approle.json +role_id=$(cat /vault/approle.json | jq -r ".data.role_id") +role_secret_id=$(cat /vault/approle.json | jq -r ".data.secret_id") +echo "VAULT_ROLE_ID=$role_id" > /vault/vault.env +echo "VAULT_ROLE_SECRET_ID=$role_secret_id" >> /vault/vault.env wait + diff --git a/service.go b/service.go index 4a4f0e9d2..414009449 100644 --- a/service.go +++ b/service.go @@ -8,7 +8,6 @@ import ( "database/sql" "net/http" "net/url" - "os" "strconv" "strings" "time" @@ -107,11 +106,11 @@ type Params struct { // call. This is mostly useful for testing. DisableConnectionCache bool - // VaultSecretFile is the path of the file containing the secret to - // use with the vault server. If this is empty then no attempt will - // be made to use a vault server and JIMM will store everything in - // it's local database. - VaultSecretFile string + // VaultRoleID is the AppRole role ID. + VaultRoleID string + + // VaultRoleSecretID is the AppRole secret ID. + VaultRoleSecretID string // VaultAddress is the URL of a vault server that will be used to // store secrets for JIMM. If this is empty then the default @@ -441,6 +440,14 @@ func openDB(ctx context.Context, dsn string) (*gorm.DB, error) { func (s *Service) setupCredentialStore(ctx context.Context, p Params) error { const op = errors.Op("newSecretStore") + + // Only enable Postgres storage for secrets if explicitly enabled. + if p.InsecureSecretStorage { + zapctx.Warn(ctx, "using plaintext postgres for secret storage") + s.jimm.CredentialStore = &s.jimm.Database + return nil + } + vs, err := newVaultStore(ctx, p) if err != nil { zapctx.Error(ctx, "Vault Store error", zap.Error(err)) @@ -451,40 +458,22 @@ func (s *Service) setupCredentialStore(ctx context.Context, p Params) error { return nil } - // Only enable Postgres storage for secrets if explicitly enabled. - if p.InsecureSecretStorage { - zapctx.Warn(ctx, "using plaintext postgres for secret storage") - s.jimm.CredentialStore = &s.jimm.Database - return nil - } // Currently jimm will start without a credential store but // functionality will be limited. return nil } func newVaultStore(ctx context.Context, p Params) (jimmcreds.CredentialStore, error) { - if p.VaultSecretFile == "" { + if p.VaultRoleID == "" || p.VaultRoleSecretID == "" { return nil, nil } zapctx.Info(ctx, "configuring vault client", zap.String("VaultAddress", p.VaultAddress), zap.String("VaultPath", p.VaultPath), - zap.String("VaultSecretFile", p.VaultSecretFile), - zap.String("VaultAuthPath", p.VaultAuthPath), + zap.String("VaultRoleID", p.VaultRoleID), ) servermon.VaultConfigured.Inc() - f, err := os.Open(p.VaultSecretFile) - if err != nil { - return nil, err - } - defer f.Close() - s, err := vaultapi.ParseSecret(f) - if err != nil || s == nil { - zapctx.Error(ctx, "failed to parse vault secret from file") - return nil, err - } - cfg := vaultapi.DefaultConfig() if p.VaultAddress != "" { cfg.Address = p.VaultAddress @@ -496,10 +485,10 @@ func newVaultStore(ctx context.Context, p Params) (jimmcreds.CredentialStore, er } return &vault.VaultStore{ - Client: client, - AuthSecret: s.Data, - AuthPath: p.VaultAuthPath, - KVPath: p.VaultPath, + Client: client, + RoleID: p.VaultRoleID, + RoleSecretID: p.VaultRoleSecretID, + KVPath: p.VaultPath, }, nil } diff --git a/service_test.go b/service_test.go index b0fa86112..eba70964a 100644 --- a/service_test.go +++ b/service_test.go @@ -130,12 +130,12 @@ func TestVault(t *testing.T) { ofgaClient, _, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) - vaultClient, _, creds, _ := jimmtest.VaultClient(c, ".") + vaultClient, _, roleID, roleSecretID, _ := jimmtest.VaultClient(c, ".") p := jimmtest.NewTestJimmParams(c) p.VaultAddress = "http://localhost:8200" - p.VaultAuthPath = "/auth/approle/login" p.VaultPath = "/jimm-kv/" - p.VaultSecretFile = "./local/vault/approle.json" + p.VaultRoleID = roleID + p.VaultRoleSecretID = roleSecretID p.OpenFGAParams = cofgaParamsToJIMMOpenFGAParams(*cofgaParams) svc, err := jimm.NewService(ctx, p) c.Assert(err, qt.IsNil) @@ -175,10 +175,10 @@ func TestVault(t *testing.T) { c.Assert(err, qt.IsNil) store := vault.VaultStore{ - Client: vaultClient, - AuthSecret: creds, - AuthPath: p.VaultAuthPath, - KVPath: p.VaultPath, + Client: vaultClient, + RoleID: roleID, + RoleSecretID: roleSecretID, + KVPath: p.VaultPath, } attr, err := store.Get(context.Background(), names.NewCloudCredentialTag("test/bob@canonical.com/test-1")) c.Assert(err, qt.IsNil) From 03610c207af57a596377b99d97eecab2bf3f0b60 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Tue, 30 Apr 2024 09:19:01 +0200 Subject: [PATCH 111/126] CSS-8240 New openfga relation (#1198) * Updated k8s charm * Update machine charm --- .../lib/charms/openfga_k8s/v0/openfga.py | 164 ------- .../lib/charms/openfga_k8s/v1/openfga.py | 424 ++++++++++++++++++ charms/jimm-k8s/src/charm.py | 26 +- charms/jimm-k8s/tests/unit/test_charm.py | 5 +- .../jimm/lib/charms/openfga_k8s/v0/openfga.py | 164 ------- .../jimm/lib/charms/openfga_k8s/v1/openfga.py | 423 +++++++++++++++++ charms/jimm/src/charm.py | 25 +- charms/jimm/tests/test_charm.py | 32 +- 8 files changed, 880 insertions(+), 383 deletions(-) delete mode 100644 charms/jimm-k8s/lib/charms/openfga_k8s/v0/openfga.py create mode 100644 charms/jimm-k8s/lib/charms/openfga_k8s/v1/openfga.py delete mode 100644 charms/jimm/lib/charms/openfga_k8s/v0/openfga.py create mode 100644 charms/jimm/lib/charms/openfga_k8s/v1/openfga.py diff --git a/charms/jimm-k8s/lib/charms/openfga_k8s/v0/openfga.py b/charms/jimm-k8s/lib/charms/openfga_k8s/v0/openfga.py deleted file mode 100644 index 4cd3e8a31..000000000 --- a/charms/jimm-k8s/lib/charms/openfga_k8s/v0/openfga.py +++ /dev/null @@ -1,164 +0,0 @@ -"""# Interface Library for OpenFGA - -This library wraps relation endpoints using the `openfga` interface -and provides a Python API for requesting OpenFGA authorization model -stores to be created. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.openfga_k8s.v0.openfga -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - openfga: - interface: openfga -``` - -Then, to initialise the library: -```python -from charms.openfga_k8s.v0.openfga import ( - OpenFGARequires, - OpenFGAStoreCreateEvent, -) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.openfga = OpenFGARequires(self, "test-openfga-store") - self.framework.observe( - self.openfga.on.openfga_store_created, - self._on_openfga_store_created, - ) - - def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): - if not self.unit.is_leader(): - return - - if not event.store_id: - return - - logger.info("store id {}".format(event.store_id)) - logger.info("token {}".format(event.token)) - logger.info("address {}".format(event.address)) - logger.info("port {}".format(event.port)) - logger.info("scheme {}".format(event.scheme)) - - if event.token_secret_id: - secret = self.model.get_secret(id=event.token_secret_id) - content = secret.get_content() - # and get the token with content["token"] -``` - -""" - -import logging - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object - -# The unique Charmhub library identifier, never change it -LIBID = "216f28cfeea4447b8a576f01bfbecdf5" - -# Increment this major API version when introducing breaking changes -LIBAPI = 0 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 4 - -logger = logging.getLogger(__name__) - -RELATION_NAME = "openfga" - - -class OpenFGAEvent(RelationEvent): - """Base class for OpenFGA events.""" - - @property - def store_id(self): - return self.relation.data[self.relation.app].get("store_id", "") - - @property - def token_secret_id(self): - return self.relation.data[self.relation.app].get("token_secret_id", "") - - @property - def token(self): - return self.relation.data[self.relation.app].get("token", "") - - @property - def address(self): - return self.relation.data[self.relation.app].get("address", "") - - @property - def scheme(self): - return self.relation.data[self.relation.app].get("scheme", "") - - @property - def port(self): - return self.relation.data[self.relation.app].get("port", "") - - -class OpenFGAStoreCreateEvent(OpenFGAEvent): - """ - Event emitted when a new OpenFGA store is created - for use on this relation. - """ - - -class OpenFGAEvents(CharmEvents): - """Custom charm events.""" - - openfga_store_created = EventSource(OpenFGAStoreCreateEvent) - - -class OpenFGARequires(Object): - """This class defines the functionality for the 'requires' side of the 'openfga' relation. - - Hook events observed: - - relation-joined - - relation-changed - """ - - on = OpenFGAEvents() - - def __init__(self, charm, store_name: str): - super().__init__(charm, RELATION_NAME) - - self.framework.observe( - charm.on[RELATION_NAME].relation_joined, self._on_relation_joined - ) - self.framework.observe( - charm.on[RELATION_NAME].relation_changed, - self._on_relation_changed, - ) - - self.data = {} - self.store_name = store_name - - def _on_relation_joined(self, event: RelationJoinedEvent): - """Handle the relation-joined event.""" - # `self.unit` isn't available here, so use `self.model.unit`. - if self.model.unit.is_leader(): - event.relation.data[self.model.app]["store_name"] = self.store_name - - def _on_relation_changed(self, event: RelationChangedEvent): - """Handle the relation-changed event.""" - if self.model.unit.is_leader(): - self.on.openfga_store_created.emit( - event.relation, - app=event.app, - unit=event.unit, - ) diff --git a/charms/jimm-k8s/lib/charms/openfga_k8s/v1/openfga.py b/charms/jimm-k8s/lib/charms/openfga_k8s/v1/openfga.py new file mode 100644 index 000000000..0f187cea0 --- /dev/null +++ b/charms/jimm-k8s/lib/charms/openfga_k8s/v1/openfga.py @@ -0,0 +1,424 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""# Interface Library for OpenFGA. + +This library wraps relation endpoints using the `openfga` interface +and provides a Python API for requesting OpenFGA authorization model +stores to be created. + +## Getting Started + +To get started using the library, you just need to fetch the library using `charmcraft`. + +```shell +cd some-charm +charmcraft fetch-lib charms.openfga_k8s.v1.openfga +``` + +In the `metadata.yaml` of the charm, add the following: + +```yaml +requires: + openfga: + interface: openfga +``` + +Then, to initialise the library: +```python +from charms.openfga_k8s.v1.openfga import ( + OpenFGARequires, + OpenFGAStoreCreateEvent, +) + +class SomeCharm(CharmBase): + def __init__(self, *args): + # ... + self.openfga = OpenFGARequires(self, "test-openfga-store") + self.framework.observe( + self.openfga.on.openfga_store_created, + self._on_openfga_store_created, + ) + + def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): + if not event.store_id: + return + + info = self.openfga.get_store_info() + if not info: + return + + logger.info("store id {}".format(info.store_id)) + logger.info("token {}".format(info.token)) + logger.info("grpc_api_url {}".format(info.grpc_api_url)) + logger.info("http_api_url {}".format(info.http_api_url)) + +``` + +The OpenFGA charm will attempt to use Juju secrets to pass the token +to the requiring charm. However if the Juju version does not support secrets it will +fall back to passing plaintext token via relation data. +""" + +import json +import logging +from typing import Dict, MutableMapping, Optional, Union + +import pydantic +from ops import ( + CharmBase, + Handle, + HookEvent, + Relation, + RelationCreatedEvent, + RelationDepartedEvent, + TooManyRelatedAppsError, +) +from ops.charm import CharmEvents, RelationChangedEvent, RelationEvent +from ops.framework import EventSource, Object +from pydantic import BaseModel, Field, validator +from typing_extensions import Self + +# The unique Charmhub library identifier, never change it +LIBID = "216f28cfeea4447b8a576f01bfbecdf5" + +# Increment this major API version when introducing breaking changes +LIBAPI = 1 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 1 +PYDEPS = ["pydantic<2.0"] + +logger = logging.getLogger(__name__) +BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} +RELATION_NAME = "openfga" +OPENFGA_TOKEN_FIELD = "token" + + +class OpenfgaError(RuntimeError): + """Base class for custom errors raised by this library.""" + + +class DataValidationError(OpenfgaError): + """Raised when data validation fails on relation data.""" + + +class DatabagModel(BaseModel): + """Base databag model.""" + + class Config: + """Pydantic config.""" + + allow_population_by_field_name = True + """Allow instantiating this class by field name (instead of forcing alias).""" + + @classmethod + def _load_value(cls, v: str) -> Union[Dict, str]: + try: + return json.loads(v) + except json.JSONDecodeError: + return v + + @classmethod + def load(cls, databag: MutableMapping) -> Self: + """Load this model from a Juju databag.""" + try: + data = { + k: cls._load_value(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS + } + except json.JSONDecodeError: + logger.error(f"invalid databag contents: expecting json. {databag}") + raise + + return cls.parse_raw(json.dumps(data)) # type: ignore + + def dump(self, databag: Optional[MutableMapping] = None) -> MutableMapping: + """Write the contents of this model to Juju databag.""" + if databag is None: + databag = {} + + dct = self.dict() + for key, field in self.__fields__.items(): # type: ignore + value = dct[key] + if value is None: + continue + databag[field.alias or key] = ( + json.dumps(value) if not isinstance(value, (str)) else value + ) + + return databag + + +class OpenfgaRequirerAppData(DatabagModel): + """Openfga requirer application databag model.""" + + store_name: str = Field(description="The store name the application requires") + + +class OpenfgaProviderAppData(DatabagModel): + """Openfga requirer application databag model.""" + + store_id: Optional[str] = Field(description="The store_id", default=None) + token: Optional[str] = Field(description="The token", default=None) + token_secret_id: Optional[str] = Field( + description="The juju secret_id which can be used to retrieve the token", + default=None, + ) + grpc_api_url: str = Field(description="The openfga server GRPC address") + http_api_url: str = Field(description="The openfga server HTTP address") + + @validator("token_secret_id", pre=True) + def validate_token(cls, v: str, values: Dict) -> str: # noqa: N805 + """Validate token_secret_id arg.""" + if not v and not values["token"]: + raise ValueError("invalid scheme: neither of token and token_secret_id were defined") + return v + + +class OpenFGAStoreCreateEvent(HookEvent): + """Event emitted when a new OpenFGA store is created.""" + + def __init__(self, handle: Handle, store_id: str): + super().__init__(handle) + self.store_id = store_id + + def snapshot(self) -> Dict: + """Save event.""" + return { + "store_id": self.store_id, + } + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + self.store_id = snapshot["store_id"] + + +class OpenFGAStoreRemovedEvent(HookEvent): + """Event emitted when a new OpenFGA store is removed.""" + + +class OpenFGARequirerEvents(CharmEvents): + """Custom charm events.""" + + openfga_store_created = EventSource(OpenFGAStoreCreateEvent) + openfga_store_removed = EventSource(OpenFGAStoreRemovedEvent) + + +class OpenFGARequires(Object): + """This class defines the functionality for the 'requires' side of the 'openfga' relation. + + Hook events observed: + - relation-created + - relation-changed + - relation-departed + """ + + on = OpenFGARequirerEvents() + + def __init__( + self, charm: CharmBase, store_name: str, relation_name: str = RELATION_NAME + ) -> None: + super().__init__(charm, relation_name) + self.charm = charm + self.relation_name = relation_name + self.store_name = store_name + + self.framework.observe(charm.on[relation_name].relation_created, self._on_relation_created) + self.framework.observe( + charm.on[relation_name].relation_changed, + self._on_relation_changed, + ) + self.framework.observe( + charm.on[relation_name].relation_departed, + self._on_relation_departed, + ) + + def _on_relation_created(self, event: RelationCreatedEvent) -> None: + """Handle the relation-created event.""" + if not self.model.unit.is_leader(): + return + + databag = event.relation.data[self.model.app] + OpenfgaRequirerAppData(store_name=self.store_name).dump(databag) + + def _on_relation_changed(self, event: RelationChangedEvent) -> None: + """Handle the relation-changed event.""" + if not (app := event.relation.app): + return + databag = event.relation.data[app] + try: + data = OpenfgaProviderAppData.load(databag) + except pydantic.ValidationError as e: + logger.error(e) + return + + self.on.openfga_store_created.emit(store_id=data.store_id) + + def _on_relation_departed(self, event: RelationDepartedEvent) -> None: + """Handle the relation-departed event.""" + self.on.openfga_store_removed.emit() + + def _get_relation(self, relation_id: Optional[int] = None) -> Optional[Relation]: + try: + relation = self.model.get_relation(self.relation_name, relation_id=relation_id) + except TooManyRelatedAppsError: + raise RuntimeError("More than one relations are defined. Please provide a relation_id") + if not relation or not relation.app: + return None + return relation + + def get_store_info(self) -> Optional[OpenfgaProviderAppData]: + """Get the OpenFGA store and server info.""" + if not (relation := self._get_relation()): + return None + if not relation.app: + return None + + databag = relation.data[relation.app] + try: + data = OpenfgaProviderAppData.load(databag) + except pydantic.ValidationError: + return None + + if data.token_secret_id: + token_secret = self.model.get_secret(id=data.token_secret_id) + token = token_secret.get_content()["token"] + data.token = token + + return data + + +class OpenFGAStoreRequestEvent(RelationEvent): + """Event emitted when a new OpenFGA store is requested.""" + + def __init__(self, handle: Handle, relation: Relation, store_name: str) -> None: + super().__init__(handle, relation) + self.store_name = store_name + + def snapshot(self) -> Dict: + """Save event.""" + dct = super().snapshot() + dct["store_name"] = self.store_name + return dct + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + super().restore(snapshot) + self.store_name = snapshot["store_name"] + + +class OpenFGAProviderEvents(CharmEvents): + """Custom charm events.""" + + openfga_store_requested = EventSource(OpenFGAStoreRequestEvent) + + +class OpenFGAProvider(Object): + """Requirer side of the openfga relation.""" + + on = OpenFGAProviderEvents() + + def __init__( + self, + charm: CharmBase, + relation_name: str = RELATION_NAME, + http_port: Optional[str] = "8080", + grpc_port: Optional[str] = "8081", + scheme: Optional[str] = "http", + ): + super().__init__(charm, relation_name) + self.charm = charm + self.relation_name = relation_name + self.http_port = http_port + self.grpc_port = grpc_port + self.scheme = scheme + + self.framework.observe( + charm.on[relation_name].relation_changed, + self._on_relation_changed, + ) + + def _on_relation_changed(self, event: RelationChangedEvent) -> None: + if not (app := event.app): + return + data = event.relation.data[app] + if not data: + logger.info("No relation data available.") + return + + try: + data = OpenfgaRequirerAppData.load(data) + except pydantic.ValidationError: + return + + self.on.openfga_store_requested.emit(event.relation, store_name=data.store_name) + + def _get_http_url(self, relation: Relation) -> str: + address = self.model.get_binding(relation).network.ingress_address.exploded + return f"{self.scheme}://{address}:{self.http_port}" + + def _get_grpc_url(self, relation: Relation) -> str: + address = self.model.get_binding(relation).network.ingress_address.exploded + return f"{self.scheme}://{address}:{self.grpc_port}" + + def update_relation_info( + self, + store_id: str, + grpc_api_url: Optional[str] = None, + http_api_url: Optional[str] = None, + token: Optional[str] = None, + token_secret_id: Optional[str] = None, + relation_id: Optional[int] = None, + ) -> None: + """Update a relation databag.""" + if not self.model.unit.is_leader(): + return + + relation = self.model.get_relation(self.relation_name, relation_id) + if not relation or not relation.app: + return + + if not grpc_api_url: + grpc_api_url = self._get_grpc_url(relation=relation) + if not http_api_url: + http_api_url = self._get_http_url(relation=relation) + + data = OpenfgaProviderAppData( + store_id=store_id, + grpc_api_url=grpc_api_url, + http_api_url=http_api_url, + token_secret_id=token_secret_id, + token=token, + ) + databag = relation.data[self.charm.app] + + try: + data.dump(databag) + except pydantic.ValidationError as e: + msg = "failed to validate app data" + logger.info(msg, exc_info=True) + raise DataValidationError(msg) from e + + def update_server_info( + self, grpc_api_url: Optional[str] = None, http_api_url: Optional[str] = None + ) -> None: + """Update all the relations databags with the server info.""" + if not self.model.unit.is_leader(): + return + + for relation in self.model.relations[self.relation_name]: + grpc_url = grpc_api_url + http_url = http_api_url + if not grpc_api_url: + grpc_url = self._get_grpc_url(relation=relation) + if not http_api_url: + http_url = self._get_http_url(relation=relation) + data = OpenfgaProviderAppData(grpc_api_url=grpc_url, http_api_url=http_url) + + try: + data.dump(relation.data[self.model.app]) + except pydantic.ValidationError as e: + msg = "failed to validate app data" + logger.info(msg, exc_info=True) + raise DataValidationError(msg) from e diff --git a/charms/jimm-k8s/src/charm.py b/charms/jimm-k8s/src/charm.py index 5a755baae..a07304a82 100755 --- a/charms/jimm-k8s/src/charm.py +++ b/charms/jimm-k8s/src/charm.py @@ -17,7 +17,7 @@ import json import logging import secrets -from urllib.parse import urljoin +from urllib.parse import urljoin, urlparse import requests from charms.data_platform_libs.v0.database_requires import ( @@ -28,7 +28,7 @@ from charms.hydra.v0.oauth import ClientConfig, OAuthInfoChangedEvent, OAuthRequirer from charms.loki_k8s.v0.loki_push_api import LogProxyConsumer from charms.nginx_ingress_integrator.v0.nginx_route import require_nginx_route -from charms.openfga_k8s.v0.openfga import OpenFGARequires, OpenFGAStoreCreateEvent +from charms.openfga_k8s.v1.openfga import OpenFGARequires, OpenFGAStoreCreateEvent from charms.prometheus_k8s.v0.prometheus_scrape import MetricsEndpointProvider from charms.tls_certificates_interface.v1.tls_certificates import ( CertificateAvailableEvent, @@ -521,17 +521,17 @@ def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): if not event.store_id: return - token = event.token - if event.token_secret_id: - secret = self.model.get_secret(id=event.token_secret_id) - secret_content = secret.get_content() - token = secret_content["token"] - - self._state.openfga_store_id = event.store_id - self._state.openfga_token = token - self._state.openfga_address = event.address - self._state.openfga_port = event.port - self._state.openfga_scheme = event.scheme + info = self.openfga.get_store_info() + if not info: + logger.warning("openfga info not ready yet") + return + + self._state.openfga_store_id = info.store_id + self._state.openfga_token = info.token + o = urlparse(info.http_api_url) + self._state.openfga_address = o.hostname + self._state.openfga_port = o.port + self._state.openfga_scheme = o.scheme self._update_workload(event) diff --git a/charms/jimm-k8s/tests/unit/test_charm.py b/charms/jimm-k8s/tests/unit/test_charm.py index 8ca9018ab..cdee15f83 100644 --- a/charms/jimm-k8s/tests/unit/test_charm.py +++ b/charms/jimm-k8s/tests/unit/test_charm.py @@ -27,9 +27,8 @@ } OPENFGA_PROVIDER_INFO = { - "address": "openfga.localhost", - "port": "8080", - "scheme": "http", + "http_api_url": "http://openfga.localhost:8080", + "grpc_api_url": "grpc://openfga.localhost:8090", "store_id": "fake-store-id", "token": "fake-token", } diff --git a/charms/jimm/lib/charms/openfga_k8s/v0/openfga.py b/charms/jimm/lib/charms/openfga_k8s/v0/openfga.py deleted file mode 100644 index 09d86f20e..000000000 --- a/charms/jimm/lib/charms/openfga_k8s/v0/openfga.py +++ /dev/null @@ -1,164 +0,0 @@ -"""# Interface Library for OpenFGA - -This library wraps relation endpoints using the `openfga` interface -and provides a Python API for requesting OpenFGA authorization model -stores to be created. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.openfga_k8s.v0.openfga -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - openfga: - interface: openfga -``` - -Then, to initialise the library: -```python -from charms.openfga_k8s.v0.openfga import ( - OpenFGARequires, - OpenFGAStoreCreateEvent, -) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.openfga = OpenFGARequires(self, "test-openfga-store") - self.framework.observe( - self.openfga.on.openfga_store_created, - self._on_openfga_store_created, - ) - - def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): - if not self.unit.is_leader(): - return - - if not event.store_id: - return - - logger.info("store id {}".format(event.store_id)) - logger.info("token {}".format(event.token)) - logger.info("address {}".format(event.address)) - logger.info("port {}".format(event.port)) - logger.info("scheme {}".format(event.scheme)) - - if event.token_secret_id: - secret = self.model.get_secret(id=event.token_secret_id) - content = secret.get_content() - # and get the token with content["token"] -``` - -""" - -import logging - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object - -# The unique Charmhub library identifier, never change it -LIBID = "216f28cfeea4447b8a576f01bfbecdf5" - -# Increment this major API version when introducing breaking changes -LIBAPI = 0 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 4 - -logger = logging.getLogger(__name__) - -RELATION_NAME = "openfga" - - -class OpenFGAEvent(RelationEvent): - """Base class for OpenFGA events.""" - - @property - def store_id(self): - return self.relation.data[self.relation.app].get("store_id") - - @property - def token_secret_id(self): - return self.relation.data[self.relation.app].get("token_secret_id") - - @property - def token(self): - return self.relation.data[self.relation.app].get("token") - - @property - def address(self): - return self.relation.data[self.relation.app].get("address") - - @property - def scheme(self): - return self.relation.data[self.relation.app].get("scheme") - - @property - def port(self): - return self.relation.data[self.relation.app].get("port") - - -class OpenFGAStoreCreateEvent(OpenFGAEvent): - """ - Event emitted when a new OpenFGA store is created - for use on this relation. - """ - - -class OpenFGAEvents(CharmEvents): - """Custom charm events.""" - - openfga_store_created = EventSource(OpenFGAStoreCreateEvent) - - -class OpenFGARequires(Object): - """This class defines the functionality for the 'requires' side of the 'openfga' relation. - - Hook events observed: - - relation-joined - - relation-changed - """ - - on = OpenFGAEvents() - - def __init__(self, charm, store_name: str): - super().__init__(charm, RELATION_NAME) - - self.framework.observe( - charm.on[RELATION_NAME].relation_joined, self._on_relation_joined - ) - self.framework.observe( - charm.on[RELATION_NAME].relation_changed, - self._on_relation_changed, - ) - - self.data = {} - self.store_name = store_name - - def _on_relation_joined(self, event: RelationJoinedEvent): - """Handle the relation-joined event.""" - # `self.unit` isn't available here, so use `self.model.unit`. - if self.model.unit.is_leader(): - event.relation.data[self.model.app]["store_name"] = self.store_name - - def _on_relation_changed(self, event: RelationChangedEvent): - """Handle the relation-changed event.""" - if self.model.unit.is_leader(): - self.on.openfga_store_created.emit( - event.relation, - app=event.app, - unit=event.unit, - ) diff --git a/charms/jimm/lib/charms/openfga_k8s/v1/openfga.py b/charms/jimm/lib/charms/openfga_k8s/v1/openfga.py new file mode 100644 index 000000000..66b20902d --- /dev/null +++ b/charms/jimm/lib/charms/openfga_k8s/v1/openfga.py @@ -0,0 +1,423 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""# Interface Library for OpenFGA. + +This library wraps relation endpoints using the `openfga` interface +and provides a Python API for requesting OpenFGA authorization model +stores to be created. + +## Getting Started + +To get started using the library, you just need to fetch the library using `charmcraft`. + +```shell +cd some-charm +charmcraft fetch-lib charms.openfga_k8s.v1.openfga +``` + +In the `metadata.yaml` of the charm, add the following: + +```yaml +requires: + openfga: + interface: openfga +``` + +Then, to initialise the library: +```python +from charms.openfga_k8s.v1.openfga import ( + OpenFGARequires, + OpenFGAStoreCreateEvent, +) + +class SomeCharm(CharmBase): + def __init__(self, *args): + # ... + self.openfga = OpenFGARequires(self, "test-openfga-store") + self.framework.observe( + self.openfga.on.openfga_store_created, + self._on_openfga_store_created, + ) + + def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): + if not event.store_id: + return + + info = self.openfga.get_store_info() + if not info: + return + + logger.info("store id {}".format(info.store_id)) + logger.info("token {}".format(info.token)) + logger.info("grpc_api_url {}".format(info.grpc_api_url)) + logger.info("http_api_url {}".format(info.http_api_url)) + +``` + +The OpenFGA charm will attempt to use Juju secrets to pass the token +to the requiring charm. However if the Juju version does not support secrets it will +fall back to passing plaintext token via relation data. +""" + +import json +import logging +from typing import Dict, MutableMapping, Optional, Union + +import pydantic +from ops import ( + CharmBase, + Handle, + HookEvent, + Relation, + RelationCreatedEvent, + RelationDepartedEvent, + TooManyRelatedAppsError, +) +from ops.charm import CharmEvents, RelationChangedEvent, RelationEvent +from ops.framework import EventSource, Object +from pydantic import BaseModel, Field, validator +from typing_extensions import Self + +# The unique Charmhub library identifier, never change it +LIBID = "216f28cfeea4447b8a576f01bfbecdf5" + +# Increment this major API version when introducing breaking changes +LIBAPI = 1 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 1 +PYDEPS = ["pydantic<2.0"] + +logger = logging.getLogger(__name__) +BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} +RELATION_NAME = "openfga" +OPENFGA_TOKEN_FIELD = "token" + + +class OpenfgaError(RuntimeError): + """Base class for custom errors raised by this library.""" + + +class DataValidationError(OpenfgaError): + """Raised when data validation fails on relation data.""" + + +class DatabagModel(BaseModel): + """Base databag model.""" + + class Config: + """Pydantic config.""" + + allow_population_by_field_name = True + """Allow instantiating this class by field name (instead of forcing alias).""" + + @classmethod + def _load_value(cls, v: str) -> Union[Dict, str]: + try: + return json.loads(v) + except json.JSONDecodeError: + return v + + @classmethod + def load(cls, databag: MutableMapping) -> Self: + """Load this model from a Juju databag.""" + try: + data = { + k: cls._load_value(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS + } + except json.JSONDecodeError: + logger.error(f"invalid databag contents: expecting json. {databag}") + raise + + return cls.parse_raw(json.dumps(data)) # type: ignore + + def dump(self, databag: Optional[MutableMapping] = None) -> MutableMapping: + """Write the contents of this model to Juju databag.""" + if databag is None: + databag = {} + + dct = self.dict() + for key, field in self.__fields__.items(): # type: ignore + value = dct[key] + if value is None: + continue + databag[field.alias or key] = ( + json.dumps(value) if not isinstance(value, (str)) else value + ) + + return databag + + +class OpenfgaRequirerAppData(DatabagModel): + """Openfga requirer application databag model.""" + + store_name: str = Field(description="The store name the application requires") + + +class OpenfgaProviderAppData(DatabagModel): + """Openfga requirer application databag model.""" + + store_id: Optional[str] = Field(description="The store_id", default=None) + token: Optional[str] = Field(description="The token", default=None) + token_secret_id: Optional[str] = Field( + description="The juju secret_id which can be used to retrieve the token", + default=None, + ) + grpc_api_url: str = Field(description="The openfga server GRPC address") + http_api_url: str = Field(description="The openfga server HTTP address") + + @validator("token_secret_id", pre=True) + def validate_token(cls, v: str, values: Dict) -> str: # noqa: N805 + """Validate token_secret_id arg.""" + if not v and not values["token"]: + raise ValueError("invalid scheme: neither of token and token_secret_id were defined") + return v + + +class OpenFGAStoreCreateEvent(HookEvent): + """Event emitted when a new OpenFGA store is created.""" + + def __init__(self, handle: Handle, store_id: str): + super().__init__(handle) + self.store_id = store_id + + def snapshot(self) -> Dict: + """Save event.""" + return { + "store_id": self.store_id, + } + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + self.store_id = snapshot["store_id"] + + +class OpenFGAStoreRemovedEvent(HookEvent): + """Event emitted when a new OpenFGA store is removed.""" + + +class OpenFGARequirerEvents(CharmEvents): + """Custom charm events.""" + + openfga_store_created = EventSource(OpenFGAStoreCreateEvent) + openfga_store_removed = EventSource(OpenFGAStoreRemovedEvent) + + +class OpenFGARequires(Object): + """This class defines the functionality for the 'requires' side of the 'openfga' relation. + + Hook events observed: + - relation-created + - relation-changed + - relation-departed + """ + + on = OpenFGARequirerEvents() + + def __init__( + self, charm: CharmBase, store_name: str, relation_name: str = RELATION_NAME + ) -> None: + super().__init__(charm, relation_name) + self.charm = charm + self.relation_name = relation_name + self.store_name = store_name + + self.framework.observe(charm.on[relation_name].relation_created, self._on_relation_created) + self.framework.observe( + charm.on[relation_name].relation_changed, + self._on_relation_changed, + ) + self.framework.observe( + charm.on[relation_name].relation_departed, + self._on_relation_departed, + ) + + def _on_relation_created(self, event: RelationCreatedEvent) -> None: + """Handle the relation-created event.""" + if not self.model.unit.is_leader(): + return + + databag = event.relation.data[self.model.app] + OpenfgaRequirerAppData(store_name=self.store_name).dump(databag) + + def _on_relation_changed(self, event: RelationChangedEvent) -> None: + """Handle the relation-changed event.""" + if not (app := event.relation.app): + return + databag = event.relation.data[app] + try: + data = OpenfgaProviderAppData.load(databag) + except pydantic.ValidationError: + return + + self.on.openfga_store_created.emit(store_id=data.store_id) + + def _on_relation_departed(self, event: RelationDepartedEvent) -> None: + """Handle the relation-departed event.""" + self.on.openfga_store_removed.emit() + + def _get_relation(self, relation_id: Optional[int] = None) -> Optional[Relation]: + try: + relation = self.model.get_relation(self.relation_name, relation_id=relation_id) + except TooManyRelatedAppsError: + raise RuntimeError("More than one relations are defined. Please provide a relation_id") + if not relation or not relation.app: + return None + return relation + + def get_store_info(self) -> Optional[OpenfgaProviderAppData]: + """Get the OpenFGA store and server info.""" + if not (relation := self._get_relation()): + return None + if not relation.app: + return None + + databag = relation.data[relation.app] + try: + data = OpenfgaProviderAppData.load(databag) + except pydantic.ValidationError: + return None + + if data.token_secret_id: + token_secret = self.model.get_secret(id=data.token_secret_id) + token = token_secret.get_content()["token"] + data.token = token + + return data + + +class OpenFGAStoreRequestEvent(RelationEvent): + """Event emitted when a new OpenFGA store is requested.""" + + def __init__(self, handle: Handle, relation: Relation, store_name: str) -> None: + super().__init__(handle, relation) + self.store_name = store_name + + def snapshot(self) -> Dict: + """Save event.""" + dct = super().snapshot() + dct["store_name"] = self.store_name + return dct + + def restore(self, snapshot: Dict) -> None: + """Restore event.""" + super().restore(snapshot) + self.store_name = snapshot["store_name"] + + +class OpenFGAProviderEvents(CharmEvents): + """Custom charm events.""" + + openfga_store_requested = EventSource(OpenFGAStoreRequestEvent) + + +class OpenFGAProvider(Object): + """Requirer side of the openfga relation.""" + + on = OpenFGAProviderEvents() + + def __init__( + self, + charm: CharmBase, + relation_name: str = RELATION_NAME, + http_port: Optional[str] = "8080", + grpc_port: Optional[str] = "8081", + scheme: Optional[str] = "http", + ): + super().__init__(charm, relation_name) + self.charm = charm + self.relation_name = relation_name + self.http_port = http_port + self.grpc_port = grpc_port + self.scheme = scheme + + self.framework.observe( + charm.on[relation_name].relation_changed, + self._on_relation_changed, + ) + + def _on_relation_changed(self, event: RelationChangedEvent) -> None: + if not (app := event.app): + return + data = event.relation.data[app] + if not data: + logger.info("No relation data available.") + return + + try: + data = OpenfgaRequirerAppData.load(data) + except pydantic.ValidationError: + return + + self.on.openfga_store_requested.emit(event.relation, store_name=data.store_name) + + def _get_http_url(self, relation: Relation) -> str: + address = self.model.get_binding(relation).network.ingress_address.exploded + return f"{self.scheme}://{address}:{self.http_port}" + + def _get_grpc_url(self, relation: Relation) -> str: + address = self.model.get_binding(relation).network.ingress_address.exploded + return f"{self.scheme}://{address}:{self.grpc_port}" + + def update_relation_info( + self, + store_id: str, + grpc_api_url: Optional[str] = None, + http_api_url: Optional[str] = None, + token: Optional[str] = None, + token_secret_id: Optional[str] = None, + relation_id: Optional[int] = None, + ) -> None: + """Update a relation databag.""" + if not self.model.unit.is_leader(): + return + + relation = self.model.get_relation(self.relation_name, relation_id) + if not relation or not relation.app: + return + + if not grpc_api_url: + grpc_api_url = self._get_grpc_url(relation=relation) + if not http_api_url: + http_api_url = self._get_http_url(relation=relation) + + data = OpenfgaProviderAppData( + store_id=store_id, + grpc_api_url=grpc_api_url, + http_api_url=http_api_url, + token_secret_id=token_secret_id, + token=token, + ) + databag = relation.data[self.charm.app] + + try: + data.dump(databag) + except pydantic.ValidationError as e: + msg = "failed to validate app data" + logger.info(msg, exc_info=True) + raise DataValidationError(msg) from e + + def update_server_info( + self, grpc_api_url: Optional[str] = None, http_api_url: Optional[str] = None + ) -> None: + """Update all the relations databags with the server info.""" + if not self.model.unit.is_leader(): + return + + for relation in self.model.relations[self.relation_name]: + grpc_url = grpc_api_url + http_url = http_api_url + if not grpc_api_url: + grpc_url = self._get_grpc_url(relation=relation) + if not http_api_url: + http_url = self._get_http_url(relation=relation) + data = OpenfgaProviderAppData(grpc_api_url=grpc_url, http_api_url=http_url) + + try: + data.dump(relation.data[self.model.app]) + except pydantic.ValidationError as e: + msg = "failed to validate app data" + logger.info(msg, exc_info=True) + raise DataValidationError(msg) from e diff --git a/charms/jimm/src/charm.py b/charms/jimm/src/charm.py index 25dfefbdb..0e0e39edb 100755 --- a/charms/jimm/src/charm.py +++ b/charms/jimm/src/charm.py @@ -11,7 +11,7 @@ import socket import subprocess import urllib -from urllib.parse import urljoin +from urllib.parse import urljoin, urlparse import hvac from charmhelpers.contrib.charmsupport.nrpe import NRPE @@ -21,7 +21,7 @@ ) from charms.grafana_agent.v0.cos_agent import COSAgentProvider from charms.hydra.v0.oauth import ClientConfig, OAuthInfoChangedEvent, OAuthRequirer -from charms.openfga_k8s.v0.openfga import OpenFGARequires, OpenFGAStoreCreateEvent +from charms.openfga_k8s.v1.openfga import OpenFGARequires, OpenFGAStoreCreateEvent from jinja2 import Environment, FileSystemLoader from ops.main import main from ops.model import ( @@ -417,19 +417,18 @@ def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): if not event.store_id: return - token = event.token - if event.token_secret_id: - logger.error("token secret {}".format(event.token_secret_id)) - secret = self.model.get_secret(id=event.token_secret_id) - secret_content = secret.get_content() - token = secret_content["token"] + info = self.openfga.get_store_info() + if not info: + logger.warning("openfga info not ready yet") + return + o = urlparse(info.http_api_url) args = { - "openfga_host": event.address, - "openfga_port": event.port, - "openfga_scheme": event.scheme, - "openfga_store": event.store_id, - "openfga_token": token, + "openfga_host": o.hostname, + "openfga_port": o.port, + "openfga_scheme": o.scheme, + "openfga_store": info.store_id, + "openfga_token": info.token, } with open(self._env_filename(OPENFGA_PART), "wt") as f: diff --git a/charms/jimm/tests/test_charm.py b/charms/jimm/tests/test_charm.py index 9a54b8d2c..d2de0c56f 100644 --- a/charms/jimm/tests/test_charm.py +++ b/charms/jimm/tests/test_charm.py @@ -34,9 +34,8 @@ } OPENFGA_PROVIDER_INFO = { - "address": "openfga.localhost", - "port": "8080", - "scheme": "http", + "http_api_url": "http://openfga.localhost:8080", + "grpc_api_url": "grpc://openfga.localhost:8090", "store_id": "fake-store-id", "token": "fake-token", } @@ -538,35 +537,16 @@ def test_dashboard_relation_joined(self): self.assertEqual(data["is_juju"], "False") def test_openfga_relation_changed(self): - id = self.harness.add_relation("openfga", "openfga") - self.harness.add_relation_unit(id, "openfga/0") - - ofga = self.harness.model.get_app("openfga") - secret = ofga.add_secret({"token": "test-secret-token"}) - - self.harness.update_relation_data( - id, - "openfga", - { - "store_id": "test-store", - "token_secret_id": secret.id, - "address": "test-address", - "port": "8080", - "scheme": "http", - }, - ) - - relation = self.harness.model.get_relation("openfga", id) - self.harness.charm.openfga.on.openfga_store_created.emit(relation) + self.add_openfga_relation() with open(self.harness.charm._env_filename("openfga")) as f: lines = f.readlines() - self.assertEqual(lines[0].strip(), "OPENFGA_HOST=test-address") + self.assertEqual(lines[0].strip(), "OPENFGA_HOST=openfga.localhost") self.assertEqual(lines[1].strip(), "OPENFGA_PORT=8080") self.assertEqual(lines[2].strip(), "OPENFGA_SCHEME=http") - self.assertEqual(lines[3].strip(), "OPENFGA_STORE=test-store") - self.assertEqual(lines[4].strip(), "OPENFGA_TOKEN=test-secret-token") + self.assertEqual(lines[3].strip(), "OPENFGA_STORE=fake-store-id") + self.assertEqual(lines[4].strip(), "OPENFGA_TOKEN=fake-token") def test_insecure_secret_storage(self): """Test that the flag for insecure secret storage is only generated when explicitly requested.""" From 58f389336fd4b2c05cea003e36339791a114316f Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Tue, 30 Apr 2024 11:05:31 +0100 Subject: [PATCH 112/126] small cosmetic changes (#1201) --- .gitignore | 1 + Makefile | 1 - docker-compose.yaml | 4 +--- local/README.md | 2 +- local/authy/main.go | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 1e63a1908..8e4046690 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ /local/vault/approle.json local/vault/approle.json local/vault/roleid.txt +local/vault/vault.env *.crt *.key diff --git a/Makefile b/Makefile index 513cb26a0..5a617aad3 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,6 @@ test-env-cleanup: dev-env-setup: sysdeps certs @touch ./local/vault/approle.json && touch ./local/vault/roleid.txt && touch ./local/vault/vault.env @make version/commit.txt && make version/version.txt - @go mod vendor dev-env: @docker compose --profile dev up --force-recreate diff --git a/docker-compose.yaml b/docker-compose.yaml index 086ba25e7..d14898671 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,3 @@ -version: "3.9" - services: traefik: image: "traefik:2.9" @@ -30,7 +28,7 @@ services: # working_dir value has to be the same of mapped volume hostname: jimm.localhost working_dir: /jimm - container_name: jimmy + container_name: jimm entrypoint: - bash - -c diff --git a/local/README.md b/local/README.md index fe6bdbce3..7541c3bf5 100644 --- a/local/README.md +++ b/local/README.md @@ -56,7 +56,7 @@ Note that you can export an environment variable `CONTROLLER_NAME` and re-run st controllers that will be controlled by JIMM. 1. `juju unregister jimm-dev` - Unregister any other local JIMM you have. -2. `juju login jimm.localhost -c jimm-dev` - Login to local JIMM with `jimm:jimm`. (If you name the controller jimm-dev, the script will pick it up!) +2. `juju login jimm.localhost -c jimm-dev` - Login to local JIMM with `Username: jimm-test, Password: password`. (If you name the controller jimm-dev, the script will pick it up!) 3. `./local/jimm/setup-controller.sh` - Performs controller setup. 4. `./local/jimm/add-controller.sh` - A local script to do many of the manual steps for us. See script for more details. 5. `juju add-model test` - Adds a model to qa-controller via JIMM. diff --git a/local/authy/main.go b/local/authy/main.go index c0929308b..6aee0ca70 100644 --- a/local/authy/main.go +++ b/local/authy/main.go @@ -175,5 +175,5 @@ func main() { fmt.Println() fmt.Println(string(maccaroonieswoonies)) fmt.Println() - fmt.Println("Copy the macaroons, head to the JIMMY postman collection and update your local collection variable for API_AUTH.") + fmt.Println("Copy the macaroons, head to the JIMM postman collection and update your local collection variable for API_AUTH.") } From 548aa2dc72926c370df484a6f382b077bb1b1b25 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Tue, 30 Apr 2024 12:35:11 +0200 Subject: [PATCH 113/126] Improve k8s charm test coverage (#1199) * Postgres and openfga action test improvements - Added tests for create-auth-model action. - Added tests for Postgres relation. - Updated database lib to the latest maintained. * Tests for update status and certificates relation * Move test mock scope --- .../data_platform_libs/v0/data_interfaces.py | 3465 +++++++++++++++++ .../v0/database_requires.py | 496 --- charms/jimm-k8s/src/charm.py | 43 +- charms/jimm-k8s/tests/unit/test_charm.py | 107 +- 4 files changed, 3576 insertions(+), 535 deletions(-) create mode 100644 charms/jimm-k8s/lib/charms/data_platform_libs/v0/data_interfaces.py delete mode 100644 charms/jimm-k8s/lib/charms/data_platform_libs/v0/database_requires.py diff --git a/charms/jimm-k8s/lib/charms/data_platform_libs/v0/data_interfaces.py b/charms/jimm-k8s/lib/charms/data_platform_libs/v0/data_interfaces.py new file mode 100644 index 000000000..3ce69e155 --- /dev/null +++ b/charms/jimm-k8s/lib/charms/data_platform_libs/v0/data_interfaces.py @@ -0,0 +1,3465 @@ +# Copyright 2023 Canonical Ltd. +# +# 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. + +r"""Library to manage the relation for the data-platform products. + +This library contains the Requires and Provides classes for handling the relation +between an application and multiple managed application supported by the data-team: +MySQL, Postgresql, MongoDB, Redis, and Kafka. + +### Database (MySQL, Postgresql, MongoDB, and Redis) + +#### Requires Charm +This library is a uniform interface to a selection of common database +metadata, with added custom events that add convenience to database management, +and methods to consume the application related data. + + +Following an example of using the DatabaseCreatedEvent, in the context of the +application charm code: + +```python + +from charms.data_platform_libs.v0.data_interfaces import ( + DatabaseCreatedEvent, + DatabaseRequires, +) + +class ApplicationCharm(CharmBase): + # Application charm that connects to database charms. + + def __init__(self, *args): + super().__init__(*args) + + # Charm events defined in the database requires charm library. + self.database = DatabaseRequires(self, relation_name="database", database_name="database") + self.framework.observe(self.database.on.database_created, self._on_database_created) + + def _on_database_created(self, event: DatabaseCreatedEvent) -> None: + # Handle the created database + + # Create configuration file for app + config_file = self._render_app_config_file( + event.username, + event.password, + event.endpoints, + ) + + # Start application with rendered configuration + self._start_application(config_file) + + # Set active status + self.unit.status = ActiveStatus("received database credentials") +``` + +As shown above, the library provides some custom events to handle specific situations, +which are listed below: + +- database_created: event emitted when the requested database is created. +- endpoints_changed: event emitted when the read/write endpoints of the database have changed. +- read_only_endpoints_changed: event emitted when the read-only endpoints of the database + have changed. Event is not triggered if read/write endpoints changed too. + +If it is needed to connect multiple database clusters to the same relation endpoint +the application charm can implement the same code as if it would connect to only +one database cluster (like the above code example). + +To differentiate multiple clusters connected to the same relation endpoint +the application charm can use the name of the remote application: + +```python + +def _on_database_created(self, event: DatabaseCreatedEvent) -> None: + # Get the remote app name of the cluster that triggered this event + cluster = event.relation.app.name +``` + +It is also possible to provide an alias for each different database cluster/relation. + +So, it is possible to differentiate the clusters in two ways. +The first is to use the remote application name, i.e., `event.relation.app.name`, as above. + +The second way is to use different event handlers to handle each cluster events. +The implementation would be something like the following code: + +```python + +from charms.data_platform_libs.v0.data_interfaces import ( + DatabaseCreatedEvent, + DatabaseRequires, +) + +class ApplicationCharm(CharmBase): + # Application charm that connects to database charms. + + def __init__(self, *args): + super().__init__(*args) + + # Define the cluster aliases and one handler for each cluster database created event. + self.database = DatabaseRequires( + self, + relation_name="database", + database_name="database", + relations_aliases = ["cluster1", "cluster2"], + ) + self.framework.observe( + self.database.on.cluster1_database_created, self._on_cluster1_database_created + ) + self.framework.observe( + self.database.on.cluster2_database_created, self._on_cluster2_database_created + ) + + def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: + # Handle the created database on the cluster named cluster1 + + # Create configuration file for app + config_file = self._render_app_config_file( + event.username, + event.password, + event.endpoints, + ) + ... + + def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: + # Handle the created database on the cluster named cluster2 + + # Create configuration file for app + config_file = self._render_app_config_file( + event.username, + event.password, + event.endpoints, + ) + ... + +``` + +When it's needed to check whether a plugin (extension) is enabled on the PostgreSQL +charm, you can use the is_postgresql_plugin_enabled method. To use that, you need to +add the following dependency to your charmcraft.yaml file: + +```yaml + +parts: + charm: + charm-binary-python-packages: + - psycopg[binary] + +``` + +### Provider Charm + +Following an example of using the DatabaseRequestedEvent, in the context of the +database charm code: + +```python +from charms.data_platform_libs.v0.data_interfaces import DatabaseProvides + +class SampleCharm(CharmBase): + + def __init__(self, *args): + super().__init__(*args) + # Charm events defined in the database provides charm library. + self.provided_database = DatabaseProvides(self, relation_name="database") + self.framework.observe(self.provided_database.on.database_requested, + self._on_database_requested) + # Database generic helper + self.database = DatabaseHelper() + + def _on_database_requested(self, event: DatabaseRequestedEvent) -> None: + # Handle the event triggered by a new database requested in the relation + # Retrieve the database name using the charm library. + db_name = event.database + # generate a new user credential + username = self.database.generate_user() + password = self.database.generate_password() + # set the credentials for the relation + self.provided_database.set_credentials(event.relation.id, username, password) + # set other variables for the relation event.set_tls("False") +``` +As shown above, the library provides a custom event (database_requested) to handle +the situation when an application charm requests a new database to be created. +It's preferred to subscribe to this event instead of relation changed event to avoid +creating a new database when other information other than a database name is +exchanged in the relation databag. + +### Kafka + +This library is the interface to use and interact with the Kafka charm. This library contains +custom events that add convenience to manage Kafka, and provides methods to consume the +application related data. + +#### Requirer Charm + +```python + +from charms.data_platform_libs.v0.data_interfaces import ( + BootstrapServerChangedEvent, + KafkaRequires, + TopicCreatedEvent, +) + +class ApplicationCharm(CharmBase): + + def __init__(self, *args): + super().__init__(*args) + self.kafka = KafkaRequires(self, "kafka_client", "test-topic") + self.framework.observe( + self.kafka.on.bootstrap_server_changed, self._on_kafka_bootstrap_server_changed + ) + self.framework.observe( + self.kafka.on.topic_created, self._on_kafka_topic_created + ) + + def _on_kafka_bootstrap_server_changed(self, event: BootstrapServerChangedEvent): + # Event triggered when a bootstrap server was changed for this application + + new_bootstrap_server = event.bootstrap_server + ... + + def _on_kafka_topic_created(self, event: TopicCreatedEvent): + # Event triggered when a topic was created for this application + username = event.username + password = event.password + tls = event.tls + tls_ca= event.tls_ca + bootstrap_server event.bootstrap_server + consumer_group_prefic = event.consumer_group_prefix + zookeeper_uris = event.zookeeper_uris + ... + +``` + +As shown above, the library provides some custom events to handle specific situations, +which are listed below: + +- topic_created: event emitted when the requested topic is created. +- bootstrap_server_changed: event emitted when the bootstrap server have changed. +- credential_changed: event emitted when the credentials of Kafka changed. + +### Provider Charm + +Following the previous example, this is an example of the provider charm. + +```python +class SampleCharm(CharmBase): + +from charms.data_platform_libs.v0.data_interfaces import ( + KafkaProvides, + TopicRequestedEvent, +) + + def __init__(self, *args): + super().__init__(*args) + + # Default charm events. + self.framework.observe(self.on.start, self._on_start) + + # Charm events defined in the Kafka Provides charm library. + self.kafka_provider = KafkaProvides(self, relation_name="kafka_client") + self.framework.observe(self.kafka_provider.on.topic_requested, self._on_topic_requested) + # Kafka generic helper + self.kafka = KafkaHelper() + + def _on_topic_requested(self, event: TopicRequestedEvent): + # Handle the on_topic_requested event. + + topic = event.topic + relation_id = event.relation.id + # set connection info in the databag relation + self.kafka_provider.set_bootstrap_server(relation_id, self.kafka.get_bootstrap_server()) + self.kafka_provider.set_credentials(relation_id, username=username, password=password) + self.kafka_provider.set_consumer_group_prefix(relation_id, ...) + self.kafka_provider.set_tls(relation_id, "False") + self.kafka_provider.set_zookeeper_uris(relation_id, ...) + +``` +As shown above, the library provides a custom event (topic_requested) to handle +the situation when an application charm requests a new topic to be created. +It is preferred to subscribe to this event instead of relation changed event to avoid +creating a new topic when other information other than a topic name is +exchanged in the relation databag. +""" + +import copy +import json +import logging +from abc import ABC, abstractmethod +from collections import UserDict, namedtuple +from datetime import datetime +from enum import Enum +from typing import ( + Callable, + Dict, + ItemsView, + KeysView, + List, + Optional, + Set, + Tuple, + Union, + ValuesView, +) + +from ops import JujuVersion, Model, Secret, SecretInfo, SecretNotFoundError +from ops.charm import ( + CharmBase, + CharmEvents, + RelationChangedEvent, + RelationCreatedEvent, + RelationEvent, + SecretChangedEvent, +) +from ops.framework import EventSource, Object +from ops.model import Application, ModelError, Relation, Unit + +# The unique Charmhub library identifier, never change it +LIBID = "6c3e6b6680d64e9c89e611d1a15f65be" + +# Increment this major API version when introducing breaking changes +LIBAPI = 0 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 34 + +PYDEPS = ["ops>=2.0.0"] + +logger = logging.getLogger(__name__) + +Diff = namedtuple("Diff", "added changed deleted") +Diff.__doc__ = """ +A tuple for storing the diff between two data mappings. + +added - keys that were added +changed - keys that still exist but have new values +deleted - key that were deleted""" + + +PROV_SECRET_PREFIX = "secret-" +REQ_SECRET_FIELDS = "requested-secrets" +GROUP_MAPPING_FIELD = "secret_group_mapping" +GROUP_SEPARATOR = "@" + + +class SecretGroup(str): + """Secret groups specific type.""" + + +class SecretGroupsAggregate(str): + """Secret groups with option to extend with additional constants.""" + + def __init__(self): + self.USER = SecretGroup("user") + self.TLS = SecretGroup("tls") + self.EXTRA = SecretGroup("extra") + + def __setattr__(self, name, value): + """Setting internal constants.""" + if name in self.__dict__: + raise RuntimeError("Can't set constant!") + else: + super().__setattr__(name, SecretGroup(value)) + + def groups(self) -> list: + """Return the list of stored SecretGroups.""" + return list(self.__dict__.values()) + + def get_group(self, group: str) -> Optional[SecretGroup]: + """If the input str translates to a group name, return that.""" + return SecretGroup(group) if group in self.groups() else None + + +SECRET_GROUPS = SecretGroupsAggregate() + + +class DataInterfacesError(Exception): + """Common ancestor for DataInterfaces related exceptions.""" + + +class SecretError(DataInterfacesError): + """Common ancestor for Secrets related exceptions.""" + + +class SecretAlreadyExistsError(SecretError): + """A secret that was to be added already exists.""" + + +class SecretsUnavailableError(SecretError): + """Secrets aren't yet available for Juju version used.""" + + +class SecretsIllegalUpdateError(SecretError): + """Secrets aren't yet available for Juju version used.""" + + +class IllegalOperationError(DataInterfacesError): + """To be used when an operation is not allowed to be performed.""" + + +def get_encoded_dict( + relation: Relation, member: Union[Unit, Application], field: str +) -> Optional[Dict[str, str]]: + """Retrieve and decode an encoded field from relation data.""" + data = json.loads(relation.data[member].get(field, "{}")) + if isinstance(data, dict): + return data + logger.error("Unexpected datatype for %s instead of dict.", str(data)) + + +def get_encoded_list( + relation: Relation, member: Union[Unit, Application], field: str +) -> Optional[List[str]]: + """Retrieve and decode an encoded field from relation data.""" + data = json.loads(relation.data[member].get(field, "[]")) + if isinstance(data, list): + return data + logger.error("Unexpected datatype for %s instead of list.", str(data)) + + +def set_encoded_field( + relation: Relation, + member: Union[Unit, Application], + field: str, + value: Union[str, list, Dict[str, str]], +) -> None: + """Set an encoded field from relation data.""" + relation.data[member].update({field: json.dumps(value)}) + + +def diff(event: RelationChangedEvent, bucket: Optional[Union[Unit, Application]]) -> Diff: + """Retrieves the diff of the data in the relation changed databag. + + Args: + event: relation changed event. + bucket: bucket of the databag (app or unit) + + Returns: + a Diff instance containing the added, deleted and changed + keys from the event relation databag. + """ + # Retrieve the old data from the data key in the application relation databag. + if not bucket: + return Diff([], [], []) + + old_data = get_encoded_dict(event.relation, bucket, "data") + + if not old_data: + old_data = {} + + # Retrieve the new data from the event relation databag. + new_data = ( + {key: value for key, value in event.relation.data[event.app].items() if key != "data"} + if event.app + else {} + ) + + # These are the keys that were added to the databag and triggered this event. + added = new_data.keys() - old_data.keys() # pyright: ignore [reportAssignmentType] + # These are the keys that were removed from the databag and triggered this event. + deleted = old_data.keys() - new_data.keys() # pyright: ignore [reportAssignmentType] + # These are the keys that already existed in the databag, + # but had their values changed. + changed = { + key + for key in old_data.keys() & new_data.keys() # pyright: ignore [reportAssignmentType] + if old_data[key] != new_data[key] # pyright: ignore [reportAssignmentType] + } + # Convert the new_data to a serializable format and save it for a next diff check. + set_encoded_field(event.relation, bucket, "data", new_data) + + # Return the diff with all possible changes. + return Diff(added, changed, deleted) + + +def leader_only(f): + """Decorator to ensure that only leader can perform given operation.""" + + def wrapper(self, *args, **kwargs): + if self.component == self.local_app and not self.local_unit.is_leader(): + logger.error( + "This operation (%s()) can only be performed by the leader unit", f.__name__ + ) + return + return f(self, *args, **kwargs) + + wrapper.leader_only = True + return wrapper + + +def juju_secrets_only(f): + """Decorator to ensure that certain operations would be only executed on Juju3.""" + + def wrapper(self, *args, **kwargs): + if not self.secrets_enabled: + raise SecretsUnavailableError("Secrets unavailable on current Juju version") + return f(self, *args, **kwargs) + + return wrapper + + +def dynamic_secrets_only(f): + """Decorator to ensure that certain operations would be only executed when NO static secrets are defined.""" + + def wrapper(self, *args, **kwargs): + if self.static_secret_fields: + raise IllegalOperationError( + "Unsafe usage of statically and dynamically defined secrets, aborting." + ) + return f(self, *args, **kwargs) + + return wrapper + + +def either_static_or_dynamic_secrets(f): + """Decorator to ensure that static and dynamic secrets won't be used in parallel.""" + + def wrapper(self, *args, **kwargs): + if self.static_secret_fields and set(self.current_secret_fields) - set( + self.static_secret_fields + ): + raise IllegalOperationError( + "Unsafe usage of statically and dynamically defined secrets, aborting." + ) + return f(self, *args, **kwargs) + + return wrapper + + +class Scope(Enum): + """Peer relations scope.""" + + APP = "app" + UNIT = "unit" + + +################################################################################ +# Secrets internal caching +################################################################################ + + +class CachedSecret: + """Locally cache a secret. + + The data structure is precisely re-using/simulating as in the actual Secret Storage + """ + + def __init__( + self, + model: Model, + component: Union[Application, Unit], + label: str, + secret_uri: Optional[str] = None, + legacy_labels: List[str] = [], + ): + self._secret_meta = None + self._secret_content = {} + self._secret_uri = secret_uri + self.label = label + self._model = model + self.component = component + self.legacy_labels = legacy_labels + self.current_label = None + + def add_secret( + self, + content: Dict[str, str], + relation: Optional[Relation] = None, + label: Optional[str] = None, + ) -> Secret: + """Create a new secret.""" + if self._secret_uri: + raise SecretAlreadyExistsError( + "Secret is already defined with uri %s", self._secret_uri + ) + + label = self.label if not label else label + + secret = self.component.add_secret(content, label=label) + if relation and relation.app != self._model.app: + # If it's not a peer relation, grant is to be applied + secret.grant(relation) + self._secret_uri = secret.id + self._secret_meta = secret + return self._secret_meta + + @property + def meta(self) -> Optional[Secret]: + """Getting cached secret meta-information.""" + if not self._secret_meta: + if not (self._secret_uri or self.label): + return + + for label in [self.label] + self.legacy_labels: + try: + self._secret_meta = self._model.get_secret(label=label) + except SecretNotFoundError: + pass + else: + if label != self.label: + self.current_label = label + break + + # If still not found, to be checked by URI, to be labelled with the proposed label + if not self._secret_meta and self._secret_uri: + self._secret_meta = self._model.get_secret(id=self._secret_uri, label=self.label) + return self._secret_meta + + def get_content(self) -> Dict[str, str]: + """Getting cached secret content.""" + if not self._secret_content: + if self.meta: + try: + self._secret_content = self.meta.get_content(refresh=True) + except (ValueError, ModelError) as err: + # https://bugs.launchpad.net/juju/+bug/2042596 + # Only triggered when 'refresh' is set + known_model_errors = [ + "ERROR either URI or label should be used for getting an owned secret but not both", + "ERROR secret owner cannot use --refresh", + ] + if isinstance(err, ModelError) and not any( + msg in str(err) for msg in known_model_errors + ): + raise + # Due to: ValueError: Secret owner cannot use refresh=True + self._secret_content = self.meta.get_content() + return self._secret_content + + def _move_to_new_label_if_needed(self): + """Helper function to re-create the secret with a different label.""" + if not self.current_label or not (self.meta and self._secret_meta): + return + + # Create a new secret with the new label + old_meta = self._secret_meta + content = self._secret_meta.get_content() + + # I wish we could just check if we are the owners of the secret... + try: + self._secret_meta = self.add_secret(content, label=self.label) + except ModelError as err: + if "this unit is not the leader" not in str(err): + raise + old_meta.remove_all_revisions() + + def set_content(self, content: Dict[str, str]) -> None: + """Setting cached secret content.""" + if not self.meta: + return + + if content: + self._move_to_new_label_if_needed() + self.meta.set_content(content) + self._secret_content = content + else: + self.meta.remove_all_revisions() + + def get_info(self) -> Optional[SecretInfo]: + """Wrapper function to apply the corresponding call on the Secret object within CachedSecret if any.""" + if self.meta: + return self.meta.get_info() + + def remove(self) -> None: + """Remove secret.""" + if not self.meta: + raise SecretsUnavailableError("Non-existent secret was attempted to be removed.") + try: + self.meta.remove_all_revisions() + except SecretNotFoundError: + pass + self._secret_content = {} + self._secret_meta = None + self._secret_uri = None + + +class SecretCache: + """A data structure storing CachedSecret objects.""" + + def __init__(self, model: Model, component: Union[Application, Unit]): + self._model = model + self.component = component + self._secrets: Dict[str, CachedSecret] = {} + + def get( + self, label: str, uri: Optional[str] = None, legacy_labels: List[str] = [] + ) -> Optional[CachedSecret]: + """Getting a secret from Juju Secret store or cache.""" + if not self._secrets.get(label): + secret = CachedSecret( + self._model, self.component, label, uri, legacy_labels=legacy_labels + ) + if secret.meta: + self._secrets[label] = secret + return self._secrets.get(label) + + def add(self, label: str, content: Dict[str, str], relation: Relation) -> CachedSecret: + """Adding a secret to Juju Secret.""" + if self._secrets.get(label): + raise SecretAlreadyExistsError(f"Secret {label} already exists") + + secret = CachedSecret(self._model, self.component, label) + secret.add_secret(content, relation) + self._secrets[label] = secret + return self._secrets[label] + + def remove(self, label: str) -> None: + """Remove a secret from the cache.""" + if secret := self.get(label): + try: + secret.remove() + self._secrets.pop(label) + except (SecretsUnavailableError, KeyError): + pass + else: + return + logging.debug("Non-existing Juju Secret was attempted to be removed %s", label) + + +################################################################################ +# Relation Data base/abstract ancestors (i.e. parent classes) +################################################################################ + + +# Base Data + + +class DataDict(UserDict): + """Python Standard Library 'dict' - like representation of Relation Data.""" + + def __init__(self, relation_data: "Data", relation_id: int): + self.relation_data = relation_data + self.relation_id = relation_id + + @property + def data(self) -> Dict[str, str]: + """Return the full content of the Abstract Relation Data dictionary.""" + result = self.relation_data.fetch_my_relation_data([self.relation_id]) + try: + result_remote = self.relation_data.fetch_relation_data([self.relation_id]) + except NotImplementedError: + result_remote = {self.relation_id: {}} + if result: + result_remote[self.relation_id].update(result[self.relation_id]) + return result_remote.get(self.relation_id, {}) + + def __setitem__(self, key: str, item: str) -> None: + """Set an item of the Abstract Relation Data dictionary.""" + self.relation_data.update_relation_data(self.relation_id, {key: item}) + + def __getitem__(self, key: str) -> str: + """Get an item of the Abstract Relation Data dictionary.""" + result = None + + # Avoiding "leader_only" error when cross-charm non-leader unit, not to report useless error + if ( + not hasattr(self.relation_data.fetch_my_relation_field, "leader_only") + or self.relation_data.component != self.relation_data.local_app + or self.relation_data.local_unit.is_leader() + ): + result = self.relation_data.fetch_my_relation_field(self.relation_id, key) + + if not result: + try: + result = self.relation_data.fetch_relation_field(self.relation_id, key) + except NotImplementedError: + pass + + if not result: + raise KeyError + return result + + def __eq__(self, d: dict) -> bool: + """Equality.""" + return self.data == d + + def __repr__(self) -> str: + """String representation Abstract Relation Data dictionary.""" + return repr(self.data) + + def __len__(self) -> int: + """Length of the Abstract Relation Data dictionary.""" + return len(self.data) + + def __delitem__(self, key: str) -> None: + """Delete an item of the Abstract Relation Data dictionary.""" + self.relation_data.delete_relation_data(self.relation_id, [key]) + + def has_key(self, key: str) -> bool: + """Does the key exist in the Abstract Relation Data dictionary?""" + return key in self.data + + def update(self, items: Dict[str, str]): + """Update the Abstract Relation Data dictionary.""" + self.relation_data.update_relation_data(self.relation_id, items) + + def keys(self) -> KeysView[str]: + """Keys of the Abstract Relation Data dictionary.""" + return self.data.keys() + + def values(self) -> ValuesView[str]: + """Values of the Abstract Relation Data dictionary.""" + return self.data.values() + + def items(self) -> ItemsView[str, str]: + """Items of the Abstract Relation Data dictionary.""" + return self.data.items() + + def pop(self, item: str) -> str: + """Pop an item of the Abstract Relation Data dictionary.""" + result = self.relation_data.fetch_my_relation_field(self.relation_id, item) + if not result: + raise KeyError(f"Item {item} doesn't exist.") + self.relation_data.delete_relation_data(self.relation_id, [item]) + return result + + def __contains__(self, item: str) -> bool: + """Does the Abstract Relation Data dictionary contain item?""" + return item in self.data.values() + + def __iter__(self): + """Iterate through the Abstract Relation Data dictionary.""" + return iter(self.data) + + def get(self, key: str, default: Optional[str] = None) -> Optional[str]: + """Safely get an item of the Abstract Relation Data dictionary.""" + try: + if result := self[key]: + return result + except KeyError: + return default + + +class Data(ABC): + """Base relation data mainpulation (abstract) class.""" + + SCOPE = Scope.APP + + # Local map to associate mappings with secrets potentially as a group + SECRET_LABEL_MAP = { + "username": SECRET_GROUPS.USER, + "password": SECRET_GROUPS.USER, + "uris": SECRET_GROUPS.USER, + "tls": SECRET_GROUPS.TLS, + "tls-ca": SECRET_GROUPS.TLS, + } + + def __init__( + self, + model: Model, + relation_name: str, + ) -> None: + self._model = model + self.local_app = self._model.app + self.local_unit = self._model.unit + self.relation_name = relation_name + self._jujuversion = None + self.component = self.local_app if self.SCOPE == Scope.APP else self.local_unit + self.secrets = SecretCache(self._model, self.component) + self.data_component = None + + @property + def relations(self) -> List[Relation]: + """The list of Relation instances associated with this relation_name.""" + return [ + relation + for relation in self._model.relations[self.relation_name] + if self._is_relation_active(relation) + ] + + @property + def secrets_enabled(self): + """Is this Juju version allowing for Secrets usage?""" + if not self._jujuversion: + self._jujuversion = JujuVersion.from_environ() + return self._jujuversion.has_secrets + + @property + def secret_label_map(self): + """Exposing secret-label map via a property -- could be overridden in descendants!""" + return self.SECRET_LABEL_MAP + + # Mandatory overrides for internal/helper methods + + @abstractmethod + def _get_relation_secret( + self, relation_id: int, group_mapping: SecretGroup, relation_name: Optional[str] = None + ) -> Optional[CachedSecret]: + """Retrieve a Juju Secret that's been stored in the relation databag.""" + raise NotImplementedError + + @abstractmethod + def _fetch_specific_relation_data( + self, relation: Relation, fields: Optional[List[str]] + ) -> Dict[str, str]: + """Fetch data available (directily or indirectly -- i.e. secrets) from the relation.""" + raise NotImplementedError + + @abstractmethod + def _fetch_my_specific_relation_data( + self, relation: Relation, fields: Optional[List[str]] + ) -> Dict[str, str]: + """Fetch data available (directily or indirectly -- i.e. secrets) from the relation for owner/this_app.""" + raise NotImplementedError + + @abstractmethod + def _update_relation_data(self, relation: Relation, data: Dict[str, str]) -> None: + """Update data available (directily or indirectly -- i.e. secrets) from the relation for owner/this_app.""" + raise NotImplementedError + + @abstractmethod + def _delete_relation_data(self, relation: Relation, fields: List[str]) -> None: + """Delete data available (directily or indirectly -- i.e. secrets) from the relation for owner/this_app.""" + raise NotImplementedError + + # Internal helper methods + + @staticmethod + def _is_relation_active(relation: Relation): + """Whether the relation is active based on contained data.""" + try: + _ = repr(relation.data) + return True + except (RuntimeError, ModelError): + return False + + @staticmethod + def _is_secret_field(field: str) -> bool: + """Is the field in question a secret reference (URI) field or not?""" + return field.startswith(PROV_SECRET_PREFIX) + + @staticmethod + def _generate_secret_label( + relation_name: str, relation_id: int, group_mapping: SecretGroup + ) -> str: + """Generate unique group_mappings for secrets within a relation context.""" + return f"{relation_name}.{relation_id}.{group_mapping}.secret" + + def _generate_secret_field_name(self, group_mapping: SecretGroup) -> str: + """Generate unique group_mappings for secrets within a relation context.""" + return f"{PROV_SECRET_PREFIX}{group_mapping}" + + def _relation_from_secret_label(self, secret_label: str) -> Optional[Relation]: + """Retrieve the relation that belongs to a secret label.""" + contents = secret_label.split(".") + + if not (contents and len(contents) >= 3): + return + + contents.pop() # ".secret" at the end + contents.pop() # Group mapping + relation_id = contents.pop() + try: + relation_id = int(relation_id) + except ValueError: + return + + # In case '.' character appeared in relation name + relation_name = ".".join(contents) + + try: + return self.get_relation(relation_name, relation_id) + except ModelError: + return + + def _group_secret_fields(self, secret_fields: List[str]) -> Dict[SecretGroup, List[str]]: + """Helper function to arrange secret mappings under their group. + + NOTE: All unrecognized items end up in the 'extra' secret bucket. + Make sure only secret fields are passed! + """ + secret_fieldnames_grouped = {} + for key in secret_fields: + if group := self.secret_label_map.get(key): + secret_fieldnames_grouped.setdefault(group, []).append(key) + else: + secret_fieldnames_grouped.setdefault(SECRET_GROUPS.EXTRA, []).append(key) + return secret_fieldnames_grouped + + def _get_group_secret_contents( + self, + relation: Relation, + group: SecretGroup, + secret_fields: Union[Set[str], List[str]] = [], + ) -> Dict[str, str]: + """Helper function to retrieve collective, requested contents of a secret.""" + if (secret := self._get_relation_secret(relation.id, group)) and ( + secret_data := secret.get_content() + ): + return { + k: v for k, v in secret_data.items() if not secret_fields or k in secret_fields + } + return {} + + def _content_for_secret_group( + self, content: Dict[str, str], secret_fields: Set[str], group_mapping: SecretGroup + ) -> Dict[str, str]: + """Select : pairs from input, that belong to this particular Secret group.""" + if group_mapping == SECRET_GROUPS.EXTRA: + return { + k: v + for k, v in content.items() + if k in secret_fields and k not in self.secret_label_map.keys() + } + + return { + k: v + for k, v in content.items() + if k in secret_fields and self.secret_label_map.get(k) == group_mapping + } + + @juju_secrets_only + def _get_relation_secret_data( + self, relation_id: int, group_mapping: SecretGroup, relation_name: Optional[str] = None + ) -> Optional[Dict[str, str]]: + """Retrieve contents of a Juju Secret that's been stored in the relation databag.""" + secret = self._get_relation_secret(relation_id, group_mapping, relation_name) + if secret: + return secret.get_content() + + # Core operations on Relation Fields manipulations (regardless whether the field is in the databag or in a secret) + # Internal functions to be called directly from transparent public interface functions (+closely related helpers) + + def _process_secret_fields( + self, + relation: Relation, + req_secret_fields: Optional[List[str]], + impacted_rel_fields: List[str], + operation: Callable, + *args, + **kwargs, + ) -> Tuple[Dict[str, str], Set[str]]: + """Isolate target secret fields of manipulation, and execute requested operation by Secret Group.""" + result = {} + + # If the relation started on a databag, we just stay on the databag + # (Rolling upgrades may result in a relation starting on databag, getting secrets enabled on-the-fly) + # self.local_app is sufficient to check (ignored if Requires, never has secrets -- works if Provider) + fallback_to_databag = ( + req_secret_fields + and (self.local_unit == self._model.unit and self.local_unit.is_leader()) + and set(req_secret_fields) & set(relation.data[self.component]) + ) + + normal_fields = set(impacted_rel_fields) + if req_secret_fields and self.secrets_enabled and not fallback_to_databag: + normal_fields = normal_fields - set(req_secret_fields) + secret_fields = set(impacted_rel_fields) - set(normal_fields) + + secret_fieldnames_grouped = self._group_secret_fields(list(secret_fields)) + + for group in secret_fieldnames_grouped: + # operation() should return nothing when all goes well + if group_result := operation(relation, group, secret_fields, *args, **kwargs): + # If "meaningful" data was returned, we take it. (Some 'operation'-s only return success/failure.) + if isinstance(group_result, dict): + result.update(group_result) + else: + # If it wasn't found as a secret, let's give it a 2nd chance as "normal" field + # Needed when Juju3 Requires meets Juju2 Provider + normal_fields |= set(secret_fieldnames_grouped[group]) + return (result, normal_fields) + + def _fetch_relation_data_without_secrets( + self, component: Union[Application, Unit], relation: Relation, fields: Optional[List[str]] + ) -> Dict[str, str]: + """Fetching databag contents when no secrets are involved. + + Since the Provider's databag is the only one holding secrest, we can apply + a simplified workflow to read the Require's side's databag. + This is used typically when the Provider side wants to read the Requires side's data, + or when the Requires side may want to read its own data. + """ + if component not in relation.data or not relation.data[component]: + return {} + + if fields: + return { + k: relation.data[component][k] for k in fields if k in relation.data[component] + } + else: + return dict(relation.data[component]) + + def _fetch_relation_data_with_secrets( + self, + component: Union[Application, Unit], + req_secret_fields: Optional[List[str]], + relation: Relation, + fields: Optional[List[str]] = None, + ) -> Dict[str, str]: + """Fetching databag contents when secrets may be involved. + + This function has internal logic to resolve if a requested field may be "hidden" + within a Relation Secret, or directly available as a databag field. Typically + used to read the Provider side's databag (eigher by the Requires side, or by + Provider side itself). + """ + result = {} + normal_fields = [] + + if not fields: + if component not in relation.data: + return {} + + all_fields = list(relation.data[component].keys()) + normal_fields = [field for field in all_fields if not self._is_secret_field(field)] + fields = normal_fields + req_secret_fields if req_secret_fields else normal_fields + + if fields: + result, normal_fields = self._process_secret_fields( + relation, req_secret_fields, fields, self._get_group_secret_contents + ) + + # Processing "normal" fields. May include leftover from what we couldn't retrieve as a secret. + # (Typically when Juju3 Requires meets Juju2 Provider) + if normal_fields: + result.update( + self._fetch_relation_data_without_secrets(component, relation, list(normal_fields)) + ) + return result + + def _update_relation_data_without_secrets( + self, component: Union[Application, Unit], relation: Relation, data: Dict[str, str] + ) -> None: + """Updating databag contents when no secrets are involved.""" + if component not in relation.data or relation.data[component] is None: + return + + if relation: + relation.data[component].update(data) + + def _delete_relation_data_without_secrets( + self, component: Union[Application, Unit], relation: Relation, fields: List[str] + ) -> None: + """Remove databag fields 'fields' from Relation.""" + if component not in relation.data or relation.data[component] is None: + return + + for field in fields: + try: + relation.data[component].pop(field) + except KeyError: + logger.debug( + "Non-existing field '%s' was attempted to be removed from the databag (relation ID: %s)", + str(field), + str(relation.id), + ) + pass + + # Public interface methods + # Handling Relation Fields seamlessly, regardless if in databag or a Juju Secret + + def as_dict(self, relation_id: int) -> UserDict: + """Dict behavior representation of the Abstract Data.""" + return DataDict(self, relation_id) + + def get_relation(self, relation_name, relation_id) -> Relation: + """Safe way of retrieving a relation.""" + relation = self._model.get_relation(relation_name, relation_id) + + if not relation: + raise DataInterfacesError( + "Relation %s %s couldn't be retrieved", relation_name, relation_id + ) + + return relation + + def fetch_relation_data( + self, + relation_ids: Optional[List[int]] = None, + fields: Optional[List[str]] = None, + relation_name: Optional[str] = None, + ) -> Dict[int, Dict[str, str]]: + """Retrieves data from relation. + + This function can be used to retrieve data from a relation + in the charm code when outside an event callback. + Function cannot be used in `*-relation-broken` events and will raise an exception. + + Returns: + a dict of the values stored in the relation data bag + for all relation instances (indexed by the relation ID). + """ + if not relation_name: + relation_name = self.relation_name + + relations = [] + if relation_ids: + relations = [ + self.get_relation(relation_name, relation_id) for relation_id in relation_ids + ] + else: + relations = self.relations + + data = {} + for relation in relations: + if not relation_ids or (relation_ids and relation.id in relation_ids): + data[relation.id] = self._fetch_specific_relation_data(relation, fields) + return data + + def fetch_relation_field( + self, relation_id: int, field: str, relation_name: Optional[str] = None + ) -> Optional[str]: + """Get a single field from the relation data.""" + return ( + self.fetch_relation_data([relation_id], [field], relation_name) + .get(relation_id, {}) + .get(field) + ) + + def fetch_my_relation_data( + self, + relation_ids: Optional[List[int]] = None, + fields: Optional[List[str]] = None, + relation_name: Optional[str] = None, + ) -> Optional[Dict[int, Dict[str, str]]]: + """Fetch data of the 'owner' (or 'this app') side of the relation. + + NOTE: Since only the leader can read the relation's 'this_app'-side + Application databag, the functionality is limited to leaders + """ + if not relation_name: + relation_name = self.relation_name + + relations = [] + if relation_ids: + relations = [ + self.get_relation(relation_name, relation_id) for relation_id in relation_ids + ] + else: + relations = self.relations + + data = {} + for relation in relations: + if not relation_ids or relation.id in relation_ids: + data[relation.id] = self._fetch_my_specific_relation_data(relation, fields) + return data + + def fetch_my_relation_field( + self, relation_id: int, field: str, relation_name: Optional[str] = None + ) -> Optional[str]: + """Get a single field from the relation data -- owner side. + + NOTE: Since only the leader can read the relation's 'this_app'-side + Application databag, the functionality is limited to leaders + """ + if relation_data := self.fetch_my_relation_data([relation_id], [field], relation_name): + return relation_data.get(relation_id, {}).get(field) + + @leader_only + def update_relation_data(self, relation_id: int, data: dict) -> None: + """Update the data within the relation.""" + relation_name = self.relation_name + relation = self.get_relation(relation_name, relation_id) + return self._update_relation_data(relation, data) + + @leader_only + def delete_relation_data(self, relation_id: int, fields: List[str]) -> None: + """Remove field from the relation.""" + relation_name = self.relation_name + relation = self.get_relation(relation_name, relation_id) + return self._delete_relation_data(relation, fields) + + +class EventHandlers(Object): + """Requires-side of the relation.""" + + def __init__(self, charm: CharmBase, relation_data: Data, unique_key: str = ""): + """Manager of base client relations.""" + if not unique_key: + unique_key = relation_data.relation_name + super().__init__(charm, unique_key) + + self.charm = charm + self.relation_data = relation_data + + self.framework.observe( + charm.on[self.relation_data.relation_name].relation_changed, + self._on_relation_changed_event, + ) + + def _diff(self, event: RelationChangedEvent) -> Diff: + """Retrieves the diff of the data in the relation changed databag. + + Args: + event: relation changed event. + + Returns: + a Diff instance containing the added, deleted and changed + keys from the event relation databag. + """ + return diff(event, self.relation_data.data_component) + + @abstractmethod + def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: + """Event emitted when the relation data has changed.""" + raise NotImplementedError + + +# Base ProviderData and RequiresData + + +class ProviderData(Data): + """Base provides-side of the data products relation.""" + + def __init__( + self, + model: Model, + relation_name: str, + ) -> None: + super().__init__(model, relation_name) + self.data_component = self.local_app + + # Private methods handling secrets + + @juju_secrets_only + def _add_relation_secret( + self, + relation: Relation, + group_mapping: SecretGroup, + secret_fields: Set[str], + data: Dict[str, str], + uri_to_databag=True, + ) -> bool: + """Add a new Juju Secret that will be registered in the relation databag.""" + secret_field = self._generate_secret_field_name(group_mapping) + if uri_to_databag and relation.data[self.component].get(secret_field): + logging.error("Secret for relation %s already exists, not adding again", relation.id) + return False + + content = self._content_for_secret_group(data, secret_fields, group_mapping) + + label = self._generate_secret_label(self.relation_name, relation.id, group_mapping) + secret = self.secrets.add(label, content, relation) + + # According to lint we may not have a Secret ID + if uri_to_databag and secret.meta and secret.meta.id: + relation.data[self.component][secret_field] = secret.meta.id + + # Return the content that was added + return True + + @juju_secrets_only + def _update_relation_secret( + self, + relation: Relation, + group_mapping: SecretGroup, + secret_fields: Set[str], + data: Dict[str, str], + ) -> bool: + """Update the contents of an existing Juju Secret, referred in the relation databag.""" + secret = self._get_relation_secret(relation.id, group_mapping) + + if not secret: + logging.error("Can't update secret for relation %s", relation.id) + return False + + content = self._content_for_secret_group(data, secret_fields, group_mapping) + + old_content = secret.get_content() + full_content = copy.deepcopy(old_content) + full_content.update(content) + secret.set_content(full_content) + + # Return True on success + return True + + def _add_or_update_relation_secrets( + self, + relation: Relation, + group: SecretGroup, + secret_fields: Set[str], + data: Dict[str, str], + uri_to_databag=True, + ) -> bool: + """Update contents for Secret group. If the Secret doesn't exist, create it.""" + if self._get_relation_secret(relation.id, group): + return self._update_relation_secret(relation, group, secret_fields, data) + else: + return self._add_relation_secret(relation, group, secret_fields, data, uri_to_databag) + + @juju_secrets_only + def _delete_relation_secret( + self, relation: Relation, group: SecretGroup, secret_fields: List[str], fields: List[str] + ) -> bool: + """Update the contents of an existing Juju Secret, referred in the relation databag.""" + secret = self._get_relation_secret(relation.id, group) + + if not secret: + logging.error("Can't delete secret for relation %s", str(relation.id)) + return False + + old_content = secret.get_content() + new_content = copy.deepcopy(old_content) + for field in fields: + try: + new_content.pop(field) + except KeyError: + logging.debug( + "Non-existing secret was attempted to be removed %s, %s", + str(relation.id), + str(field), + ) + return False + + # Remove secret from the relation if it's fully gone + if not new_content: + field = self._generate_secret_field_name(group) + try: + relation.data[self.component].pop(field) + except KeyError: + pass + label = self._generate_secret_label(self.relation_name, relation.id, group) + self.secrets.remove(label) + else: + secret.set_content(new_content) + + # Return the content that was removed + return True + + # Mandatory internal overrides + + @juju_secrets_only + def _get_relation_secret( + self, relation_id: int, group_mapping: SecretGroup, relation_name: Optional[str] = None + ) -> Optional[CachedSecret]: + """Retrieve a Juju Secret that's been stored in the relation databag.""" + if not relation_name: + relation_name = self.relation_name + + label = self._generate_secret_label(relation_name, relation_id, group_mapping) + if secret := self.secrets.get(label): + return secret + + relation = self._model.get_relation(relation_name, relation_id) + if not relation: + return + + secret_field = self._generate_secret_field_name(group_mapping) + if secret_uri := relation.data[self.local_app].get(secret_field): + return self.secrets.get(label, secret_uri) + + def _fetch_specific_relation_data( + self, relation: Relation, fields: Optional[List[str]] + ) -> Dict[str, str]: + """Fetching relation data for Provider. + + NOTE: Since all secret fields are in the Provider side of the databag, we don't need to worry about that + """ + if not relation.app: + return {} + + return self._fetch_relation_data_without_secrets(relation.app, relation, fields) + + def _fetch_my_specific_relation_data( + self, relation: Relation, fields: Optional[List[str]] + ) -> dict: + """Fetching our own relation data.""" + secret_fields = None + if relation.app: + secret_fields = get_encoded_list(relation, relation.app, REQ_SECRET_FIELDS) + + return self._fetch_relation_data_with_secrets( + self.local_app, + secret_fields, + relation, + fields, + ) + + def _update_relation_data(self, relation: Relation, data: Dict[str, str]) -> None: + """Set values for fields not caring whether it's a secret or not.""" + req_secret_fields = [] + if relation.app: + req_secret_fields = get_encoded_list(relation, relation.app, REQ_SECRET_FIELDS) + + _, normal_fields = self._process_secret_fields( + relation, + req_secret_fields, + list(data), + self._add_or_update_relation_secrets, + data=data, + ) + + normal_content = {k: v for k, v in data.items() if k in normal_fields} + self._update_relation_data_without_secrets(self.local_app, relation, normal_content) + + def _delete_relation_data(self, relation: Relation, fields: List[str]) -> None: + """Delete fields from the Relation not caring whether it's a secret or not.""" + req_secret_fields = [] + if relation.app: + req_secret_fields = get_encoded_list(relation, relation.app, REQ_SECRET_FIELDS) + + _, normal_fields = self._process_secret_fields( + relation, req_secret_fields, fields, self._delete_relation_secret, fields=fields + ) + self._delete_relation_data_without_secrets(self.local_app, relation, list(normal_fields)) + + # Public methods - "native" + + def set_credentials(self, relation_id: int, username: str, password: str) -> None: + """Set credentials. + + This function writes in the application data bag, therefore, + only the leader unit can call it. + + Args: + relation_id: the identifier for a particular relation. + username: user that was created. + password: password of the created user. + """ + self.update_relation_data(relation_id, {"username": username, "password": password}) + + def set_tls(self, relation_id: int, tls: str) -> None: + """Set whether TLS is enabled. + + Args: + relation_id: the identifier for a particular relation. + tls: whether tls is enabled (True or False). + """ + self.update_relation_data(relation_id, {"tls": tls}) + + def set_tls_ca(self, relation_id: int, tls_ca: str) -> None: + """Set the TLS CA in the application relation databag. + + Args: + relation_id: the identifier for a particular relation. + tls_ca: TLS certification authority. + """ + self.update_relation_data(relation_id, {"tls-ca": tls_ca}) + + # Public functions -- inherited + + fetch_my_relation_data = leader_only(Data.fetch_my_relation_data) + fetch_my_relation_field = leader_only(Data.fetch_my_relation_field) + + +class RequirerData(Data): + """Requirer-side of the relation.""" + + SECRET_FIELDS = ["username", "password", "tls", "tls-ca", "uris"] + + def __init__( + self, + model, + relation_name: str, + extra_user_roles: Optional[str] = None, + additional_secret_fields: Optional[List[str]] = [], + ): + """Manager of base client relations.""" + super().__init__(model, relation_name) + self.extra_user_roles = extra_user_roles + self._secret_fields = list(self.SECRET_FIELDS) + if additional_secret_fields: + self._secret_fields += additional_secret_fields + self.data_component = self.local_unit + + @property + def secret_fields(self) -> Optional[List[str]]: + """Local access to secrets field, in case they are being used.""" + if self.secrets_enabled: + return self._secret_fields + + # Internal helper functions + + def _register_secret_to_relation( + self, relation_name: str, relation_id: int, secret_id: str, group: SecretGroup + ): + """Fetch secrets and apply local label on them. + + [MAGIC HERE] + If we fetch a secret using get_secret(id=, label=), + then will be "stuck" on the Secret object, whenever it may + appear (i.e. as an event attribute, or fetched manually) on future occasions. + + This will allow us to uniquely identify the secret on Provider side (typically on + 'secret-changed' events), and map it to the corresponding relation. + """ + label = self._generate_secret_label(relation_name, relation_id, group) + + # Fetchin the Secret's meta information ensuring that it's locally getting registered with + CachedSecret(self._model, self.component, label, secret_id).meta + + def _register_secrets_to_relation(self, relation: Relation, params_name_list: List[str]): + """Make sure that secrets of the provided list are locally 'registered' from the databag. + + More on 'locally registered' magic is described in _register_secret_to_relation() method + """ + if not relation.app: + return + + for group in SECRET_GROUPS.groups(): + secret_field = self._generate_secret_field_name(group) + if secret_field in params_name_list: + if secret_uri := relation.data[relation.app].get(secret_field): + self._register_secret_to_relation( + relation.name, relation.id, secret_uri, group + ) + + def _is_resource_created_for_relation(self, relation: Relation) -> bool: + if not relation.app: + return False + + data = self.fetch_relation_data([relation.id], ["username", "password"]).get( + relation.id, {} + ) + return bool(data.get("username")) and bool(data.get("password")) + + def is_resource_created(self, relation_id: Optional[int] = None) -> bool: + """Check if the resource has been created. + + This function can be used to check if the Provider answered with data in the charm code + when outside an event callback. + + Args: + relation_id (int, optional): When provided the check is done only for the relation id + provided, otherwise the check is done for all relations + + Returns: + True or False + + Raises: + IndexError: If relation_id is provided but that relation does not exist + """ + if relation_id is not None: + try: + relation = [relation for relation in self.relations if relation.id == relation_id][ + 0 + ] + return self._is_resource_created_for_relation(relation) + except IndexError: + raise IndexError(f"relation id {relation_id} cannot be accessed") + else: + return ( + all( + self._is_resource_created_for_relation(relation) for relation in self.relations + ) + if self.relations + else False + ) + + # Mandatory internal overrides + + @juju_secrets_only + def _get_relation_secret( + self, relation_id: int, group: SecretGroup, relation_name: Optional[str] = None + ) -> Optional[CachedSecret]: + """Retrieve a Juju Secret that's been stored in the relation databag.""" + if not relation_name: + relation_name = self.relation_name + + label = self._generate_secret_label(relation_name, relation_id, group) + return self.secrets.get(label) + + def _fetch_specific_relation_data( + self, relation, fields: Optional[List[str]] = None + ) -> Dict[str, str]: + """Fetching Requirer data -- that may include secrets.""" + if not relation.app: + return {} + return self._fetch_relation_data_with_secrets( + relation.app, self.secret_fields, relation, fields + ) + + def _fetch_my_specific_relation_data(self, relation, fields: Optional[List[str]]) -> dict: + """Fetching our own relation data.""" + return self._fetch_relation_data_without_secrets(self.local_app, relation, fields) + + def _update_relation_data(self, relation: Relation, data: dict) -> None: + """Updates a set of key-value pairs in the relation. + + This function writes in the application data bag, therefore, + only the leader unit can call it. + + Args: + relation: the particular relation. + data: dict containing the key-value pairs + that should be updated in the relation. + """ + return self._update_relation_data_without_secrets(self.local_app, relation, data) + + def _delete_relation_data(self, relation: Relation, fields: List[str]) -> None: + """Deletes a set of fields from the relation. + + This function writes in the application data bag, therefore, + only the leader unit can call it. + + Args: + relation: the particular relation. + fields: list containing the field names that should be removed from the relation. + """ + return self._delete_relation_data_without_secrets(self.local_app, relation, fields) + + # Public functions -- inherited + + fetch_my_relation_data = leader_only(Data.fetch_my_relation_data) + fetch_my_relation_field = leader_only(Data.fetch_my_relation_field) + + +class RequirerEventHandlers(EventHandlers): + """Requires-side of the relation.""" + + def __init__(self, charm: CharmBase, relation_data: RequirerData, unique_key: str = ""): + """Manager of base client relations.""" + super().__init__(charm, relation_data, unique_key) + + self.framework.observe( + self.charm.on[relation_data.relation_name].relation_created, + self._on_relation_created_event, + ) + self.framework.observe( + charm.on.secret_changed, + self._on_secret_changed_event, + ) + + # Event handlers + + def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: + """Event emitted when the relation is created.""" + if not self.relation_data.local_unit.is_leader(): + return + + if self.relation_data.secret_fields: # pyright: ignore [reportAttributeAccessIssue] + set_encoded_field( + event.relation, + self.relation_data.component, + REQ_SECRET_FIELDS, + self.relation_data.secret_fields, # pyright: ignore [reportAttributeAccessIssue] + ) + + @abstractmethod + def _on_secret_changed_event(self, event: RelationChangedEvent) -> None: + """Event emitted when the relation data has changed.""" + raise NotImplementedError + + +################################################################################ +# Peer Relation Data +################################################################################ + + +class DataPeerData(RequirerData, ProviderData): + """Represents peer relations data.""" + + SECRET_FIELDS = [] + SECRET_FIELD_NAME = "internal_secret" + SECRET_LABEL_MAP = {} + + def __init__( + self, + model, + relation_name: str, + extra_user_roles: Optional[str] = None, + additional_secret_fields: Optional[List[str]] = [], + additional_secret_group_mapping: Dict[str, str] = {}, + secret_field_name: Optional[str] = None, + deleted_label: Optional[str] = None, + ): + """Manager of base client relations.""" + RequirerData.__init__( + self, + model, + relation_name, + extra_user_roles, + additional_secret_fields, + ) + self.secret_field_name = secret_field_name if secret_field_name else self.SECRET_FIELD_NAME + self.deleted_label = deleted_label + self._secret_label_map = {} + # Secrets that are being dynamically added within the scope of this event handler run + self._new_secrets = [] + self._additional_secret_group_mapping = additional_secret_group_mapping + + for group, fields in additional_secret_group_mapping.items(): + if group not in SECRET_GROUPS.groups(): + setattr(SECRET_GROUPS, group, group) + for field in fields: + secret_group = SECRET_GROUPS.get_group(group) + internal_field = self._field_to_internal_name(field, secret_group) + self._secret_label_map.setdefault(group, []).append(internal_field) + self._secret_fields.append(internal_field) + + @property + def scope(self) -> Optional[Scope]: + """Turn component information into Scope.""" + if isinstance(self.component, Application): + return Scope.APP + if isinstance(self.component, Unit): + return Scope.UNIT + + @property + def secret_label_map(self) -> Dict[str, str]: + """Property storing secret mappings.""" + return self._secret_label_map + + @property + def static_secret_fields(self) -> List[str]: + """Re-definition of the property in a way that dynamically extended list is retrieved.""" + return self._secret_fields + + @property + def secret_fields(self) -> List[str]: + """Re-definition of the property in a way that dynamically extended list is retrieved.""" + return ( + self.static_secret_fields if self.static_secret_fields else self.current_secret_fields + ) + + @property + def current_secret_fields(self) -> List[str]: + """Helper method to get all currently existing secret fields (added statically or dynamically).""" + if not self.secrets_enabled: + return [] + + if len(self._model.relations[self.relation_name]) > 1: + raise ValueError(f"More than one peer relation on {self.relation_name}") + + relation = self._model.relations[self.relation_name][0] + fields = [] + + ignores = [SECRET_GROUPS.get_group("user"), SECRET_GROUPS.get_group("tls")] + for group in SECRET_GROUPS.groups(): + if group in ignores: + continue + if content := self._get_group_secret_contents(relation, group): + fields += list(content.keys()) + return list(set(fields) | set(self._new_secrets)) + + @dynamic_secrets_only + def set_secret( + self, + relation_id: int, + field: str, + value: str, + group_mapping: Optional[SecretGroup] = None, + ) -> None: + """Public interface method to add a Relation Data field specifically as a Juju Secret. + + Args: + relation_id: ID of the relation + field: The secret field that is to be added + value: The string value of the secret + group_mapping: The name of the "secret group", in case the field is to be added to an existing secret + """ + full_field = self._field_to_internal_name(field, group_mapping) + if self.secrets_enabled and full_field not in self.current_secret_fields: + self._new_secrets.append(full_field) + if self._no_group_with_databag(field, full_field): + self.update_relation_data(relation_id, {full_field: value}) + + # Unlike for set_secret(), there's no harm using this operation with static secrets + # The restricion is only added to keep the concept clear + @dynamic_secrets_only + def get_secret( + self, + relation_id: int, + field: str, + group_mapping: Optional[SecretGroup] = None, + ) -> Optional[str]: + """Public interface method to fetch secrets only.""" + full_field = self._field_to_internal_name(field, group_mapping) + if ( + self.secrets_enabled + and full_field not in self.current_secret_fields + and field not in self.current_secret_fields + ): + return + if self._no_group_with_databag(field, full_field): + return self.fetch_my_relation_field(relation_id, full_field) + + @dynamic_secrets_only + def delete_secret( + self, + relation_id: int, + field: str, + group_mapping: Optional[SecretGroup] = None, + ) -> Optional[str]: + """Public interface method to delete secrets only.""" + full_field = self._field_to_internal_name(field, group_mapping) + if self.secrets_enabled and full_field not in self.current_secret_fields: + logger.warning(f"Secret {field} from group {group_mapping} was not found") + return + if self._no_group_with_databag(field, full_field): + self.delete_relation_data(relation_id, [full_field]) + + # Helpers + + @staticmethod + def _field_to_internal_name(field: str, group: Optional[SecretGroup]) -> str: + if not group or group == SECRET_GROUPS.EXTRA: + return field + return f"{field}{GROUP_SEPARATOR}{group}" + + @staticmethod + def _internal_name_to_field(name: str) -> Tuple[str, SecretGroup]: + parts = name.split(GROUP_SEPARATOR) + if not len(parts) > 1: + return (parts[0], SECRET_GROUPS.EXTRA) + secret_group = SECRET_GROUPS.get_group(parts[1]) + if not secret_group: + raise ValueError(f"Invalid secret field {name}") + return (parts[0], secret_group) + + def _group_secret_fields(self, secret_fields: List[str]) -> Dict[SecretGroup, List[str]]: + """Helper function to arrange secret mappings under their group. + + NOTE: All unrecognized items end up in the 'extra' secret bucket. + Make sure only secret fields are passed! + """ + secret_fieldnames_grouped = {} + for key in secret_fields: + field, group = self._internal_name_to_field(key) + secret_fieldnames_grouped.setdefault(group, []).append(field) + return secret_fieldnames_grouped + + def _content_for_secret_group( + self, content: Dict[str, str], secret_fields: Set[str], group_mapping: SecretGroup + ) -> Dict[str, str]: + """Select : pairs from input, that belong to this particular Secret group.""" + if group_mapping == SECRET_GROUPS.EXTRA: + return {k: v for k, v in content.items() if k in self.secret_fields} + return { + self._internal_name_to_field(k)[0]: v + for k, v in content.items() + if k in self.secret_fields + } + + # Backwards compatibility + + def _check_deleted_label(self, relation, fields) -> None: + """Helper function for legacy behavior.""" + current_data = self.fetch_my_relation_data([relation.id], fields) + if current_data is not None: + # Check if the secret we wanna delete actually exists + # Given the "deleted label", here we can't rely on the default mechanism (i.e. 'key not found') + if non_existent := (set(fields) & set(self.secret_fields)) - set( + current_data.get(relation.id, []) + ): + logger.debug( + "Non-existing secret %s was attempted to be removed.", + ", ".join(non_existent), + ) + + def _remove_secret_from_databag(self, relation, fields: List[str]) -> None: + """For Rolling Upgrades -- when moving from databag to secrets usage. + + Practically what happens here is to remove stuff from the databag that is + to be stored in secrets. + """ + if not self.secret_fields: + return + + secret_fields_passed = set(self.secret_fields) & set(fields) + for field in secret_fields_passed: + if self._fetch_relation_data_without_secrets(self.component, relation, [field]): + self._delete_relation_data_without_secrets(self.component, relation, [field]) + + def _remove_secret_field_name_from_databag(self, relation) -> None: + """Making sure that the old databag URI is gone. + + This action should not be executed more than once. + """ + # Nothing to do if 'internal-secret' is not in the databag + if not (relation.data[self.component].get(self._generate_secret_field_name())): + return + + # Making sure that the secret receives its label + # (This should have happened by the time we get here, rather an extra security measure.) + secret = self._get_relation_secret(relation.id) + + # Either app scope secret with leader executing, or unit scope secret + leader_or_unit_scope = self.component != self.local_app or self.local_unit.is_leader() + if secret and leader_or_unit_scope: + # Databag reference to the secret URI can be removed, now that it's labelled + relation.data[self.component].pop(self._generate_secret_field_name(), None) + + def _previous_labels(self) -> List[str]: + """Generator for legacy secret label names, for backwards compatibility.""" + result = [] + members = [self._model.app.name] + if self.scope: + members.append(self.scope.value) + result.append(f"{'.'.join(members)}") + return result + + def _no_group_with_databag(self, field: str, full_field: str) -> bool: + """Check that no secret group is attempted to be used together with databag.""" + if not self.secrets_enabled and full_field != field: + logger.error( + f"Can't access {full_field}: no secrets available (i.e. no secret groups either)." + ) + return False + return True + + # Event handlers + + def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: + """Event emitted when the relation has changed.""" + pass + + def _on_secret_changed_event(self, event: SecretChangedEvent) -> None: + """Event emitted when the secret has changed.""" + pass + + # Overrides of Relation Data handling functions + + def _generate_secret_label( + self, relation_name: str, relation_id: int, group_mapping: SecretGroup + ) -> str: + members = [relation_name, self._model.app.name] + if self.scope: + members.append(self.scope.value) + if group_mapping != SECRET_GROUPS.EXTRA: + members.append(group_mapping) + return f"{'.'.join(members)}" + + def _generate_secret_field_name(self, group_mapping: SecretGroup = SECRET_GROUPS.EXTRA) -> str: + """Generate unique group_mappings for secrets within a relation context.""" + return f"{self.secret_field_name}" + + @juju_secrets_only + def _get_relation_secret( + self, + relation_id: int, + group_mapping: SecretGroup = SECRET_GROUPS.EXTRA, + relation_name: Optional[str] = None, + ) -> Optional[CachedSecret]: + """Retrieve a Juju Secret specifically for peer relations. + + In case this code may be executed within a rolling upgrade, and we may need to + migrate secrets from the databag to labels, we make sure to stick the correct + label on the secret, and clean up the local databag. + """ + if not relation_name: + relation_name = self.relation_name + + relation = self._model.get_relation(relation_name, relation_id) + if not relation: + return + + label = self._generate_secret_label(relation_name, relation_id, group_mapping) + secret_uri = relation.data[self.component].get(self._generate_secret_field_name(), None) + + # URI or legacy label is only to applied when moving single legacy secret to a (new) label + if group_mapping == SECRET_GROUPS.EXTRA: + # Fetching the secret with fallback to URI (in case label is not yet known) + # Label would we "stuck" on the secret in case it is found + return self.secrets.get(label, secret_uri, legacy_labels=self._previous_labels()) + return self.secrets.get(label) + + def _get_group_secret_contents( + self, + relation: Relation, + group: SecretGroup, + secret_fields: Union[Set[str], List[str]] = [], + ) -> Dict[str, str]: + """Helper function to retrieve collective, requested contents of a secret.""" + secret_fields = [self._internal_name_to_field(k)[0] for k in secret_fields] + result = super()._get_group_secret_contents(relation, group, secret_fields) + if self.deleted_label: + result = {key: result[key] for key in result if result[key] != self.deleted_label} + if self._additional_secret_group_mapping: + return {self._field_to_internal_name(key, group): result[key] for key in result} + return result + + @either_static_or_dynamic_secrets + def _fetch_my_specific_relation_data( + self, relation: Relation, fields: Optional[List[str]] + ) -> Dict[str, str]: + """Fetch data available (directily or indirectly -- i.e. secrets) from the relation for owner/this_app.""" + return self._fetch_relation_data_with_secrets( + self.component, self.secret_fields, relation, fields + ) + + @either_static_or_dynamic_secrets + def _update_relation_data(self, relation: Relation, data: Dict[str, str]) -> None: + """Update data available (directily or indirectly -- i.e. secrets) from the relation for owner/this_app.""" + self._remove_secret_from_databag(relation, list(data.keys())) + _, normal_fields = self._process_secret_fields( + relation, + self.secret_fields, + list(data), + self._add_or_update_relation_secrets, + data=data, + uri_to_databag=False, + ) + self._remove_secret_field_name_from_databag(relation) + + normal_content = {k: v for k, v in data.items() if k in normal_fields} + self._update_relation_data_without_secrets(self.component, relation, normal_content) + + @either_static_or_dynamic_secrets + def _delete_relation_data(self, relation: Relation, fields: List[str]) -> None: + """Delete data available (directily or indirectly -- i.e. secrets) from the relation for owner/this_app.""" + if self.secret_fields and self.deleted_label: + # Legacy, backwards compatibility + self._check_deleted_label(relation, fields) + + _, normal_fields = self._process_secret_fields( + relation, + self.secret_fields, + fields, + self._update_relation_secret, + data={field: self.deleted_label for field in fields}, + ) + else: + _, normal_fields = self._process_secret_fields( + relation, self.secret_fields, fields, self._delete_relation_secret, fields=fields + ) + self._delete_relation_data_without_secrets(self.component, relation, list(normal_fields)) + + def fetch_relation_data( + self, + relation_ids: Optional[List[int]] = None, + fields: Optional[List[str]] = None, + relation_name: Optional[str] = None, + ) -> Dict[int, Dict[str, str]]: + """This method makes no sense for a Peer Relation.""" + raise NotImplementedError( + "Peer Relation only supports 'self-side' fetch methods: " + "fetch_my_relation_data() and fetch_my_relation_field()" + ) + + def fetch_relation_field( + self, relation_id: int, field: str, relation_name: Optional[str] = None + ) -> Optional[str]: + """This method makes no sense for a Peer Relation.""" + raise NotImplementedError( + "Peer Relation only supports 'self-side' fetch methods: " + "fetch_my_relation_data() and fetch_my_relation_field()" + ) + + # Public functions -- inherited + + fetch_my_relation_data = Data.fetch_my_relation_data + fetch_my_relation_field = Data.fetch_my_relation_field + + +class DataPeerEventHandlers(RequirerEventHandlers): + """Requires-side of the relation.""" + + def __init__(self, charm: CharmBase, relation_data: RequirerData, unique_key: str = ""): + """Manager of base client relations.""" + super().__init__(charm, relation_data, unique_key) + + def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: + """Event emitted when the relation has changed.""" + pass + + def _on_secret_changed_event(self, event: SecretChangedEvent) -> None: + """Event emitted when the secret has changed.""" + pass + + +class DataPeer(DataPeerData, DataPeerEventHandlers): + """Represents peer relations.""" + + def __init__( + self, + charm, + relation_name: str, + extra_user_roles: Optional[str] = None, + additional_secret_fields: Optional[List[str]] = [], + additional_secret_group_mapping: Dict[str, str] = {}, + secret_field_name: Optional[str] = None, + deleted_label: Optional[str] = None, + unique_key: str = "", + ): + DataPeerData.__init__( + self, + charm.model, + relation_name, + extra_user_roles, + additional_secret_fields, + additional_secret_group_mapping, + secret_field_name, + deleted_label, + ) + DataPeerEventHandlers.__init__(self, charm, self, unique_key) + + +class DataPeerUnitData(DataPeerData): + """Unit data abstraction representation.""" + + SCOPE = Scope.UNIT + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class DataPeerUnit(DataPeerUnitData, DataPeerEventHandlers): + """Unit databag representation.""" + + def __init__( + self, + charm, + relation_name: str, + extra_user_roles: Optional[str] = None, + additional_secret_fields: Optional[List[str]] = [], + additional_secret_group_mapping: Dict[str, str] = {}, + secret_field_name: Optional[str] = None, + deleted_label: Optional[str] = None, + unique_key: str = "", + ): + DataPeerData.__init__( + self, + charm.model, + relation_name, + extra_user_roles, + additional_secret_fields, + additional_secret_group_mapping, + secret_field_name, + deleted_label, + ) + DataPeerEventHandlers.__init__(self, charm, self, unique_key) + + +class DataPeerOtherUnitData(DataPeerUnitData): + """Unit data abstraction representation.""" + + def __init__(self, unit: Unit, *args, **kwargs): + super().__init__(*args, **kwargs) + self.local_unit = unit + self.component = unit + + def update_relation_data(self, relation_id: int, data: dict) -> None: + """This method makes no sense for a Other Peer Relation.""" + raise NotImplementedError("It's not possible to update data of another unit.") + + def delete_relation_data(self, relation_id: int, fields: List[str]) -> None: + """This method makes no sense for a Other Peer Relation.""" + raise NotImplementedError("It's not possible to delete data of another unit.") + + +class DataPeerOtherUnitEventHandlers(DataPeerEventHandlers): + """Requires-side of the relation.""" + + def __init__(self, charm: CharmBase, relation_data: DataPeerUnitData): + """Manager of base client relations.""" + unique_key = f"{relation_data.relation_name}-{relation_data.local_unit.name}" + super().__init__(charm, relation_data, unique_key=unique_key) + + +class DataPeerOtherUnit(DataPeerOtherUnitData, DataPeerOtherUnitEventHandlers): + """Unit databag representation for another unit than the executor.""" + + def __init__( + self, + unit: Unit, + charm: CharmBase, + relation_name: str, + extra_user_roles: Optional[str] = None, + additional_secret_fields: Optional[List[str]] = [], + additional_secret_group_mapping: Dict[str, str] = {}, + secret_field_name: Optional[str] = None, + deleted_label: Optional[str] = None, + ): + DataPeerOtherUnitData.__init__( + self, + unit, + charm.model, + relation_name, + extra_user_roles, + additional_secret_fields, + additional_secret_group_mapping, + secret_field_name, + deleted_label, + ) + DataPeerOtherUnitEventHandlers.__init__(self, charm, self) + + +################################################################################ +# Cross-charm Relatoins Data Handling and Evenets +################################################################################ + +# Generic events + + +class ExtraRoleEvent(RelationEvent): + """Base class for data events.""" + + @property + def extra_user_roles(self) -> Optional[str]: + """Returns the extra user roles that were requested.""" + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("extra-user-roles") + + +class RelationEventWithSecret(RelationEvent): + """Base class for Relation Events that need to handle secrets.""" + + @property + def _secrets(self) -> dict: + """Caching secrets to avoid fetching them each time a field is referrd. + + DON'T USE the encapsulated helper variable outside of this function + """ + if not hasattr(self, "_cached_secrets"): + self._cached_secrets = {} + return self._cached_secrets + + def _get_secret(self, group) -> Optional[Dict[str, str]]: + """Retrieveing secrets.""" + if not self.app: + return + if not self._secrets.get(group): + self._secrets[group] = None + secret_field = f"{PROV_SECRET_PREFIX}{group}" + if secret_uri := self.relation.data[self.app].get(secret_field): + secret = self.framework.model.get_secret(id=secret_uri) + self._secrets[group] = secret.get_content() + return self._secrets[group] + + @property + def secrets_enabled(self): + """Is this Juju version allowing for Secrets usage?""" + return JujuVersion.from_environ().has_secrets + + +class AuthenticationEvent(RelationEventWithSecret): + """Base class for authentication fields for events. + + The amount of logic added here is not ideal -- but this was the only way to preserve + the interface when moving to Juju Secrets + """ + + @property + def username(self) -> Optional[str]: + """Returns the created username.""" + if not self.relation.app: + return None + + if self.secrets_enabled: + secret = self._get_secret("user") + if secret: + return secret.get("username") + + return self.relation.data[self.relation.app].get("username") + + @property + def password(self) -> Optional[str]: + """Returns the password for the created user.""" + if not self.relation.app: + return None + + if self.secrets_enabled: + secret = self._get_secret("user") + if secret: + return secret.get("password") + + return self.relation.data[self.relation.app].get("password") + + @property + def tls(self) -> Optional[str]: + """Returns whether TLS is configured.""" + if not self.relation.app: + return None + + if self.secrets_enabled: + secret = self._get_secret("tls") + if secret: + return secret.get("tls") + + return self.relation.data[self.relation.app].get("tls") + + @property + def tls_ca(self) -> Optional[str]: + """Returns TLS CA.""" + if not self.relation.app: + return None + + if self.secrets_enabled: + secret = self._get_secret("tls") + if secret: + return secret.get("tls-ca") + + return self.relation.data[self.relation.app].get("tls-ca") + + +# Database related events and fields + + +class DatabaseProvidesEvent(RelationEvent): + """Base class for database events.""" + + @property + def database(self) -> Optional[str]: + """Returns the database that was requested.""" + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("database") + + +class DatabaseRequestedEvent(DatabaseProvidesEvent, ExtraRoleEvent): + """Event emitted when a new database is requested for use on this relation.""" + + @property + def external_node_connectivity(self) -> bool: + """Returns the requested external_node_connectivity field.""" + if not self.relation.app: + return False + + return ( + self.relation.data[self.relation.app].get("external-node-connectivity", "false") + == "true" + ) + + +class DatabaseProvidesEvents(CharmEvents): + """Database events. + + This class defines the events that the database can emit. + """ + + database_requested = EventSource(DatabaseRequestedEvent) + + +class DatabaseRequiresEvent(RelationEventWithSecret): + """Base class for database events.""" + + @property + def database(self) -> Optional[str]: + """Returns the database name.""" + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("database") + + @property + def endpoints(self) -> Optional[str]: + """Returns a comma separated list of read/write endpoints. + + In VM charms, this is the primary's address. + In kubernetes charms, this is the service to the primary pod. + """ + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("endpoints") + + @property + def read_only_endpoints(self) -> Optional[str]: + """Returns a comma separated list of read only endpoints. + + In VM charms, this is the address of all the secondary instances. + In kubernetes charms, this is the service to all replica pod instances. + """ + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("read-only-endpoints") + + @property + def replset(self) -> Optional[str]: + """Returns the replicaset name. + + MongoDB only. + """ + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("replset") + + @property + def uris(self) -> Optional[str]: + """Returns the connection URIs. + + MongoDB, Redis, OpenSearch. + """ + if not self.relation.app: + return None + + if self.secrets_enabled: + secret = self._get_secret("user") + if secret: + return secret.get("uris") + + return self.relation.data[self.relation.app].get("uris") + + @property + def version(self) -> Optional[str]: + """Returns the version of the database. + + Version as informed by the database daemon. + """ + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("version") + + +class DatabaseCreatedEvent(AuthenticationEvent, DatabaseRequiresEvent): + """Event emitted when a new database is created for use on this relation.""" + + +class DatabaseEndpointsChangedEvent(AuthenticationEvent, DatabaseRequiresEvent): + """Event emitted when the read/write endpoints are changed.""" + + +class DatabaseReadOnlyEndpointsChangedEvent(AuthenticationEvent, DatabaseRequiresEvent): + """Event emitted when the read only endpoints are changed.""" + + +class DatabaseRequiresEvents(CharmEvents): + """Database events. + + This class defines the events that the database can emit. + """ + + database_created = EventSource(DatabaseCreatedEvent) + endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) + read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) + + +# Database Provider and Requires + + +class DatabaseProviderData(ProviderData): + """Provider-side data of the database relations.""" + + def __init__(self, model: Model, relation_name: str) -> None: + super().__init__(model, relation_name) + + def set_database(self, relation_id: int, database_name: str) -> None: + """Set database name. + + This function writes in the application data bag, therefore, + only the leader unit can call it. + + Args: + relation_id: the identifier for a particular relation. + database_name: database name. + """ + self.update_relation_data(relation_id, {"database": database_name}) + + def set_endpoints(self, relation_id: int, connection_strings: str) -> None: + """Set database primary connections. + + This function writes in the application data bag, therefore, + only the leader unit can call it. + + In VM charms, only the primary's address should be passed as an endpoint. + In kubernetes charms, the service endpoint to the primary pod should be + passed as an endpoint. + + Args: + relation_id: the identifier for a particular relation. + connection_strings: database hosts and ports comma separated list. + """ + self.update_relation_data(relation_id, {"endpoints": connection_strings}) + + def set_read_only_endpoints(self, relation_id: int, connection_strings: str) -> None: + """Set database replicas connection strings. + + This function writes in the application data bag, therefore, + only the leader unit can call it. + + Args: + relation_id: the identifier for a particular relation. + connection_strings: database hosts and ports comma separated list. + """ + self.update_relation_data(relation_id, {"read-only-endpoints": connection_strings}) + + def set_replset(self, relation_id: int, replset: str) -> None: + """Set replica set name in the application relation databag. + + MongoDB only. + + Args: + relation_id: the identifier for a particular relation. + replset: replica set name. + """ + self.update_relation_data(relation_id, {"replset": replset}) + + def set_uris(self, relation_id: int, uris: str) -> None: + """Set the database connection URIs in the application relation databag. + + MongoDB, Redis, and OpenSearch only. + + Args: + relation_id: the identifier for a particular relation. + uris: connection URIs. + """ + self.update_relation_data(relation_id, {"uris": uris}) + + def set_version(self, relation_id: int, version: str) -> None: + """Set the database version in the application relation databag. + + Args: + relation_id: the identifier for a particular relation. + version: database version. + """ + self.update_relation_data(relation_id, {"version": version}) + + +class DatabaseProviderEventHandlers(EventHandlers): + """Provider-side of the database relation handlers.""" + + on = DatabaseProvidesEvents() # pyright: ignore [reportAssignmentType] + + def __init__( + self, charm: CharmBase, relation_data: DatabaseProviderData, unique_key: str = "" + ): + """Manager of base client relations.""" + super().__init__(charm, relation_data, unique_key) + # Just to calm down pyright, it can't parse that the same type is being used in the super() call above + self.relation_data = relation_data + + def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: + """Event emitted when the relation has changed.""" + # Leader only + if not self.relation_data.local_unit.is_leader(): + return + # Check which data has changed to emit customs events. + diff = self._diff(event) + + # Emit a database requested event if the setup key (database name and optional + # extra user roles) was added to the relation databag by the application. + if "database" in diff.added: + getattr(self.on, "database_requested").emit( + event.relation, app=event.app, unit=event.unit + ) + + +class DatabaseProvides(DatabaseProviderData, DatabaseProviderEventHandlers): + """Provider-side of the database relations.""" + + def __init__(self, charm: CharmBase, relation_name: str) -> None: + DatabaseProviderData.__init__(self, charm.model, relation_name) + DatabaseProviderEventHandlers.__init__(self, charm, self) + + +class DatabaseRequirerData(RequirerData): + """Requirer-side of the database relation.""" + + def __init__( + self, + model: Model, + relation_name: str, + database_name: str, + extra_user_roles: Optional[str] = None, + relations_aliases: Optional[List[str]] = None, + additional_secret_fields: Optional[List[str]] = [], + external_node_connectivity: bool = False, + ): + """Manager of database client relations.""" + super().__init__(model, relation_name, extra_user_roles, additional_secret_fields) + self.database = database_name + self.relations_aliases = relations_aliases + self.external_node_connectivity = external_node_connectivity + + def is_postgresql_plugin_enabled(self, plugin: str, relation_index: int = 0) -> bool: + """Returns whether a plugin is enabled in the database. + + Args: + plugin: name of the plugin to check. + relation_index: optional relation index to check the database + (default: 0 - first relation). + + PostgreSQL only. + """ + # Psycopg 3 is imported locally to avoid the need of its package installation + # when relating to a database charm other than PostgreSQL. + import psycopg + + # Return False if no relation is established. + if len(self.relations) == 0: + return False + + relation_id = self.relations[relation_index].id + host = self.fetch_relation_field(relation_id, "endpoints") + + # Return False if there is no endpoint available. + if host is None: + return False + + host = host.split(":")[0] + + content = self.fetch_relation_data([relation_id], ["username", "password"]).get( + relation_id, {} + ) + user = content.get("username") + password = content.get("password") + + connection_string = ( + f"host='{host}' dbname='{self.database}' user='{user}' password='{password}'" + ) + try: + with psycopg.connect(connection_string) as connection: + with connection.cursor() as cursor: + cursor.execute( + "SELECT TRUE FROM pg_extension WHERE extname=%s::text;", (plugin,) + ) + return cursor.fetchone() is not None + except psycopg.Error as e: + logger.exception( + f"failed to check whether {plugin} plugin is enabled in the database: %s", str(e) + ) + return False + + +class DatabaseRequirerEventHandlers(RequirerEventHandlers): + """Requires-side of the relation.""" + + on = DatabaseRequiresEvents() # pyright: ignore [reportAssignmentType] + + def __init__( + self, charm: CharmBase, relation_data: DatabaseRequirerData, unique_key: str = "" + ): + """Manager of base client relations.""" + super().__init__(charm, relation_data, unique_key) + # Just to keep lint quiet, can't resolve inheritance. The same happened in super().__init__() above + self.relation_data = relation_data + + # Define custom event names for each alias. + if self.relation_data.relations_aliases: + # Ensure the number of aliases does not exceed the maximum + # of connections allowed in the specific relation. + relation_connection_limit = self.charm.meta.requires[ + self.relation_data.relation_name + ].limit + if len(self.relation_data.relations_aliases) != relation_connection_limit: + raise ValueError( + f"The number of aliases must match the maximum number of connections allowed in the relation. " + f"Expected {relation_connection_limit}, got {len(self.relation_data.relations_aliases)}" + ) + + if self.relation_data.relations_aliases: + for relation_alias in self.relation_data.relations_aliases: + self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) + self.on.define_event( + f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent + ) + self.on.define_event( + f"{relation_alias}_read_only_endpoints_changed", + DatabaseReadOnlyEndpointsChangedEvent, + ) + + def _on_secret_changed_event(self, event: SecretChangedEvent): + """Event notifying about a new value of a secret.""" + pass + + def _assign_relation_alias(self, relation_id: int) -> None: + """Assigns an alias to a relation. + + This function writes in the unit data bag. + + Args: + relation_id: the identifier for a particular relation. + """ + # If no aliases were provided, return immediately. + if not self.relation_data.relations_aliases: + return + + # Return if an alias was already assigned to this relation + # (like when there are more than one unit joining the relation). + relation = self.charm.model.get_relation(self.relation_data.relation_name, relation_id) + if relation and relation.data[self.relation_data.local_unit].get("alias"): + return + + # Retrieve the available aliases (the ones that weren't assigned to any relation). + available_aliases = self.relation_data.relations_aliases[:] + for relation in self.charm.model.relations[self.relation_data.relation_name]: + alias = relation.data[self.relation_data.local_unit].get("alias") + if alias: + logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) + available_aliases.remove(alias) + + # Set the alias in the unit relation databag of the specific relation. + relation = self.charm.model.get_relation(self.relation_data.relation_name, relation_id) + if relation: + relation.data[self.relation_data.local_unit].update({"alias": available_aliases[0]}) + + # We need to set relation alias also on the application level so, + # it will be accessible in show-unit juju command, executed for a consumer application unit + if self.relation_data.local_unit.is_leader(): + self.relation_data.update_relation_data(relation_id, {"alias": available_aliases[0]}) + + def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: + """Emit an aliased event to a particular relation if it has an alias. + + Args: + event: the relation changed event that was received. + event_name: the name of the event to emit. + """ + alias = self._get_relation_alias(event.relation.id) + if alias: + getattr(self.on, f"{alias}_{event_name}").emit( + event.relation, app=event.app, unit=event.unit + ) + + def _get_relation_alias(self, relation_id: int) -> Optional[str]: + """Returns the relation alias. + + Args: + relation_id: the identifier for a particular relation. + + Returns: + the relation alias or None if the relation was not found. + """ + for relation in self.charm.model.relations[self.relation_data.relation_name]: + if relation.id == relation_id: + return relation.data[self.relation_data.local_unit].get("alias") + return None + + def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: + """Event emitted when the database relation is created.""" + super()._on_relation_created_event(event) + + # If relations aliases were provided, assign one to the relation. + self._assign_relation_alias(event.relation.id) + + # Sets both database and extra user roles in the relation + # if the roles are provided. Otherwise, sets only the database. + if not self.relation_data.local_unit.is_leader(): + return + + event_data = {"database": self.relation_data.database} + + if self.relation_data.extra_user_roles: + event_data["extra-user-roles"] = self.relation_data.extra_user_roles + + # set external-node-connectivity field + if self.relation_data.external_node_connectivity: + event_data["external-node-connectivity"] = "true" + + self.relation_data.update_relation_data(event.relation.id, event_data) + + def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: + """Event emitted when the database relation has changed.""" + # Check which data has changed to emit customs events. + diff = self._diff(event) + + # Register all new secrets with their labels + if any(newval for newval in diff.added if self.relation_data._is_secret_field(newval)): + self.relation_data._register_secrets_to_relation(event.relation, diff.added) + + # Check if the database is created + # (the database charm shared the credentials). + secret_field_user = self.relation_data._generate_secret_field_name(SECRET_GROUPS.USER) + if ( + "username" in diff.added and "password" in diff.added + ) or secret_field_user in diff.added: + # Emit the default event (the one without an alias). + logger.info("database created at %s", datetime.now()) + getattr(self.on, "database_created").emit( + event.relation, app=event.app, unit=event.unit + ) + + # Emit the aliased event (if any). + self._emit_aliased_event(event, "database_created") + + # To avoid unnecessary application restarts do not trigger + # “endpoints_changed“ event if “database_created“ is triggered. + return + + # Emit an endpoints changed event if the database + # added or changed this info in the relation databag. + if "endpoints" in diff.added or "endpoints" in diff.changed: + # Emit the default event (the one without an alias). + logger.info("endpoints changed on %s", datetime.now()) + getattr(self.on, "endpoints_changed").emit( + event.relation, app=event.app, unit=event.unit + ) + + # Emit the aliased event (if any). + self._emit_aliased_event(event, "endpoints_changed") + + # To avoid unnecessary application restarts do not trigger + # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. + return + + # Emit a read only endpoints changed event if the database + # added or changed this info in the relation databag. + if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: + # Emit the default event (the one without an alias). + logger.info("read-only-endpoints changed on %s", datetime.now()) + getattr(self.on, "read_only_endpoints_changed").emit( + event.relation, app=event.app, unit=event.unit + ) + + # Emit the aliased event (if any). + self._emit_aliased_event(event, "read_only_endpoints_changed") + + +class DatabaseRequires(DatabaseRequirerData, DatabaseRequirerEventHandlers): + """Provider-side of the database relations.""" + + def __init__( + self, + charm: CharmBase, + relation_name: str, + database_name: str, + extra_user_roles: Optional[str] = None, + relations_aliases: Optional[List[str]] = None, + additional_secret_fields: Optional[List[str]] = [], + external_node_connectivity: bool = False, + ): + DatabaseRequirerData.__init__( + self, + charm.model, + relation_name, + database_name, + extra_user_roles, + relations_aliases, + additional_secret_fields, + external_node_connectivity, + ) + DatabaseRequirerEventHandlers.__init__(self, charm, self) + + +################################################################################ +# Charm-specific Relations Data and Events +################################################################################ + +# Kafka Events + + +class KafkaProvidesEvent(RelationEvent): + """Base class for Kafka events.""" + + @property + def topic(self) -> Optional[str]: + """Returns the topic that was requested.""" + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("topic") + + @property + def consumer_group_prefix(self) -> Optional[str]: + """Returns the consumer-group-prefix that was requested.""" + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("consumer-group-prefix") + + +class TopicRequestedEvent(KafkaProvidesEvent, ExtraRoleEvent): + """Event emitted when a new topic is requested for use on this relation.""" + + +class KafkaProvidesEvents(CharmEvents): + """Kafka events. + + This class defines the events that the Kafka can emit. + """ + + topic_requested = EventSource(TopicRequestedEvent) + + +class KafkaRequiresEvent(RelationEvent): + """Base class for Kafka events.""" + + @property + def topic(self) -> Optional[str]: + """Returns the topic.""" + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("topic") + + @property + def bootstrap_server(self) -> Optional[str]: + """Returns a comma-separated list of broker uris.""" + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("endpoints") + + @property + def consumer_group_prefix(self) -> Optional[str]: + """Returns the consumer-group-prefix.""" + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("consumer-group-prefix") + + @property + def zookeeper_uris(self) -> Optional[str]: + """Returns a comma separated list of Zookeeper uris.""" + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("zookeeper-uris") + + +class TopicCreatedEvent(AuthenticationEvent, KafkaRequiresEvent): + """Event emitted when a new topic is created for use on this relation.""" + + +class BootstrapServerChangedEvent(AuthenticationEvent, KafkaRequiresEvent): + """Event emitted when the bootstrap server is changed.""" + + +class KafkaRequiresEvents(CharmEvents): + """Kafka events. + + This class defines the events that the Kafka can emit. + """ + + topic_created = EventSource(TopicCreatedEvent) + bootstrap_server_changed = EventSource(BootstrapServerChangedEvent) + + +# Kafka Provides and Requires + + +class KafkaProvidesData(ProviderData): + """Provider-side of the Kafka relation.""" + + def __init__(self, model: Model, relation_name: str) -> None: + super().__init__(model, relation_name) + + def set_topic(self, relation_id: int, topic: str) -> None: + """Set topic name in the application relation databag. + + Args: + relation_id: the identifier for a particular relation. + topic: the topic name. + """ + self.update_relation_data(relation_id, {"topic": topic}) + + def set_bootstrap_server(self, relation_id: int, bootstrap_server: str) -> None: + """Set the bootstrap server in the application relation databag. + + Args: + relation_id: the identifier for a particular relation. + bootstrap_server: the bootstrap server address. + """ + self.update_relation_data(relation_id, {"endpoints": bootstrap_server}) + + def set_consumer_group_prefix(self, relation_id: int, consumer_group_prefix: str) -> None: + """Set the consumer group prefix in the application relation databag. + + Args: + relation_id: the identifier for a particular relation. + consumer_group_prefix: the consumer group prefix string. + """ + self.update_relation_data(relation_id, {"consumer-group-prefix": consumer_group_prefix}) + + def set_zookeeper_uris(self, relation_id: int, zookeeper_uris: str) -> None: + """Set the zookeeper uris in the application relation databag. + + Args: + relation_id: the identifier for a particular relation. + zookeeper_uris: comma-separated list of ZooKeeper server uris. + """ + self.update_relation_data(relation_id, {"zookeeper-uris": zookeeper_uris}) + + +class KafkaProvidesEventHandlers(EventHandlers): + """Provider-side of the Kafka relation.""" + + on = KafkaProvidesEvents() # pyright: ignore [reportAssignmentType] + + def __init__(self, charm: CharmBase, relation_data: KafkaProvidesData) -> None: + super().__init__(charm, relation_data) + # Just to keep lint quiet, can't resolve inheritance. The same happened in super().__init__() above + self.relation_data = relation_data + + def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: + """Event emitted when the relation has changed.""" + # Leader only + if not self.relation_data.local_unit.is_leader(): + return + + # Check which data has changed to emit customs events. + diff = self._diff(event) + + # Emit a topic requested event if the setup key (topic name and optional + # extra user roles) was added to the relation databag by the application. + if "topic" in diff.added: + getattr(self.on, "topic_requested").emit( + event.relation, app=event.app, unit=event.unit + ) + + +class KafkaProvides(KafkaProvidesData, KafkaProvidesEventHandlers): + """Provider-side of the Kafka relation.""" + + def __init__(self, charm: CharmBase, relation_name: str) -> None: + KafkaProvidesData.__init__(self, charm.model, relation_name) + KafkaProvidesEventHandlers.__init__(self, charm, self) + + +class KafkaRequiresData(RequirerData): + """Requirer-side of the Kafka relation.""" + + def __init__( + self, + model: Model, + relation_name: str, + topic: str, + extra_user_roles: Optional[str] = None, + consumer_group_prefix: Optional[str] = None, + additional_secret_fields: Optional[List[str]] = [], + ): + """Manager of Kafka client relations.""" + super().__init__(model, relation_name, extra_user_roles, additional_secret_fields) + self.topic = topic + self.consumer_group_prefix = consumer_group_prefix or "" + + @property + def topic(self): + """Topic to use in Kafka.""" + return self._topic + + @topic.setter + def topic(self, value): + # Avoid wildcards + if value == "*": + raise ValueError(f"Error on topic '{value}', cannot be a wildcard.") + self._topic = value + + +class KafkaRequiresEventHandlers(RequirerEventHandlers): + """Requires-side of the Kafka relation.""" + + on = KafkaRequiresEvents() # pyright: ignore [reportAssignmentType] + + def __init__(self, charm: CharmBase, relation_data: KafkaRequiresData) -> None: + super().__init__(charm, relation_data) + # Just to keep lint quiet, can't resolve inheritance. The same happened in super().__init__() above + self.relation_data = relation_data + + def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: + """Event emitted when the Kafka relation is created.""" + super()._on_relation_created_event(event) + + if not self.relation_data.local_unit.is_leader(): + return + + # Sets topic, extra user roles, and "consumer-group-prefix" in the relation + relation_data = { + f: getattr(self, f.replace("-", "_"), "") + for f in ["consumer-group-prefix", "extra-user-roles", "topic"] + } + + self.relation_data.update_relation_data(event.relation.id, relation_data) + + def _on_secret_changed_event(self, event: SecretChangedEvent): + """Event notifying about a new value of a secret.""" + pass + + def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: + """Event emitted when the Kafka relation has changed.""" + # Check which data has changed to emit customs events. + diff = self._diff(event) + + # Check if the topic is created + # (the Kafka charm shared the credentials). + + # Register all new secrets with their labels + if any(newval for newval in diff.added if self.relation_data._is_secret_field(newval)): + self.relation_data._register_secrets_to_relation(event.relation, diff.added) + + secret_field_user = self.relation_data._generate_secret_field_name(SECRET_GROUPS.USER) + if ( + "username" in diff.added and "password" in diff.added + ) or secret_field_user in diff.added: + # Emit the default event (the one without an alias). + logger.info("topic created at %s", datetime.now()) + getattr(self.on, "topic_created").emit(event.relation, app=event.app, unit=event.unit) + + # To avoid unnecessary application restarts do not trigger + # “endpoints_changed“ event if “topic_created“ is triggered. + return + + # Emit an endpoints (bootstrap-server) changed event if the Kafka endpoints + # added or changed this info in the relation databag. + if "endpoints" in diff.added or "endpoints" in diff.changed: + # Emit the default event (the one without an alias). + logger.info("endpoints changed on %s", datetime.now()) + getattr(self.on, "bootstrap_server_changed").emit( + event.relation, app=event.app, unit=event.unit + ) # here check if this is the right design + return + + +class KafkaRequires(KafkaRequiresData, KafkaRequiresEventHandlers): + """Provider-side of the Kafka relation.""" + + def __init__( + self, + charm: CharmBase, + relation_name: str, + topic: str, + extra_user_roles: Optional[str] = None, + consumer_group_prefix: Optional[str] = None, + additional_secret_fields: Optional[List[str]] = [], + ) -> None: + KafkaRequiresData.__init__( + self, + charm.model, + relation_name, + topic, + extra_user_roles, + consumer_group_prefix, + additional_secret_fields, + ) + KafkaRequiresEventHandlers.__init__(self, charm, self) + + +# Opensearch related events + + +class OpenSearchProvidesEvent(RelationEvent): + """Base class for OpenSearch events.""" + + @property + def index(self) -> Optional[str]: + """Returns the index that was requested.""" + if not self.relation.app: + return None + + return self.relation.data[self.relation.app].get("index") + + +class IndexRequestedEvent(OpenSearchProvidesEvent, ExtraRoleEvent): + """Event emitted when a new index is requested for use on this relation.""" + + +class OpenSearchProvidesEvents(CharmEvents): + """OpenSearch events. + + This class defines the events that OpenSearch can emit. + """ + + index_requested = EventSource(IndexRequestedEvent) + + +class OpenSearchRequiresEvent(DatabaseRequiresEvent): + """Base class for OpenSearch requirer events.""" + + +class IndexCreatedEvent(AuthenticationEvent, OpenSearchRequiresEvent): + """Event emitted when a new index is created for use on this relation.""" + + +class OpenSearchRequiresEvents(CharmEvents): + """OpenSearch events. + + This class defines the events that the opensearch requirer can emit. + """ + + index_created = EventSource(IndexCreatedEvent) + endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) + authentication_updated = EventSource(AuthenticationEvent) + + +# OpenSearch Provides and Requires Objects + + +class OpenSearchProvidesData(ProviderData): + """Provider-side of the OpenSearch relation.""" + + def __init__(self, model: Model, relation_name: str) -> None: + super().__init__(model, relation_name) + + def set_index(self, relation_id: int, index: str) -> None: + """Set the index in the application relation databag. + + Args: + relation_id: the identifier for a particular relation. + index: the index as it is _created_ on the provider charm. This needn't match the + requested index, and can be used to present a different index name if, for example, + the requested index is invalid. + """ + self.update_relation_data(relation_id, {"index": index}) + + def set_endpoints(self, relation_id: int, endpoints: str) -> None: + """Set the endpoints in the application relation databag. + + Args: + relation_id: the identifier for a particular relation. + endpoints: the endpoint addresses for opensearch nodes. + """ + self.update_relation_data(relation_id, {"endpoints": endpoints}) + + def set_version(self, relation_id: int, version: str) -> None: + """Set the opensearch version in the application relation databag. + + Args: + relation_id: the identifier for a particular relation. + version: database version. + """ + self.update_relation_data(relation_id, {"version": version}) + + +class OpenSearchProvidesEventHandlers(EventHandlers): + """Provider-side of the OpenSearch relation.""" + + on = OpenSearchProvidesEvents() # pyright: ignore[reportAssignmentType] + + def __init__(self, charm: CharmBase, relation_data: OpenSearchProvidesData) -> None: + super().__init__(charm, relation_data) + # Just to keep lint quiet, can't resolve inheritance. The same happened in super().__init__() above + self.relation_data = relation_data + + def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: + """Event emitted when the relation has changed.""" + # Leader only + if not self.relation_data.local_unit.is_leader(): + return + # Check which data has changed to emit customs events. + diff = self._diff(event) + + # Emit an index requested event if the setup key (index name and optional extra user roles) + # have been added to the relation databag by the application. + if "index" in diff.added: + getattr(self.on, "index_requested").emit( + event.relation, app=event.app, unit=event.unit + ) + + +class OpenSearchProvides(OpenSearchProvidesData, OpenSearchProvidesEventHandlers): + """Provider-side of the OpenSearch relation.""" + + def __init__(self, charm: CharmBase, relation_name: str) -> None: + OpenSearchProvidesData.__init__(self, charm.model, relation_name) + OpenSearchProvidesEventHandlers.__init__(self, charm, self) + + +class OpenSearchRequiresData(RequirerData): + """Requires data side of the OpenSearch relation.""" + + def __init__( + self, + model: Model, + relation_name: str, + index: str, + extra_user_roles: Optional[str] = None, + additional_secret_fields: Optional[List[str]] = [], + ): + """Manager of OpenSearch client relations.""" + super().__init__(model, relation_name, extra_user_roles, additional_secret_fields) + self.index = index + + +class OpenSearchRequiresEventHandlers(RequirerEventHandlers): + """Requires events side of the OpenSearch relation.""" + + on = OpenSearchRequiresEvents() # pyright: ignore[reportAssignmentType] + + def __init__(self, charm: CharmBase, relation_data: OpenSearchRequiresData) -> None: + super().__init__(charm, relation_data) + # Just to keep lint quiet, can't resolve inheritance. The same happened in super().__init__() above + self.relation_data = relation_data + + def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: + """Event emitted when the OpenSearch relation is created.""" + super()._on_relation_created_event(event) + + if not self.relation_data.local_unit.is_leader(): + return + + # Sets both index and extra user roles in the relation if the roles are provided. + # Otherwise, sets only the index. + data = {"index": self.relation_data.index} + if self.relation_data.extra_user_roles: + data["extra-user-roles"] = self.relation_data.extra_user_roles + + self.relation_data.update_relation_data(event.relation.id, data) + + def _on_secret_changed_event(self, event: SecretChangedEvent): + """Event notifying about a new value of a secret.""" + if not event.secret.label: + return + + relation = self.relation_data._relation_from_secret_label(event.secret.label) + if not relation: + logging.info( + f"Received secret {event.secret.label} but couldn't parse, seems irrelevant" + ) + return + + if relation.app == self.charm.app: + logging.info("Secret changed event ignored for Secret Owner") + + remote_unit = None + for unit in relation.units: + if unit.app != self.charm.app: + remote_unit = unit + + logger.info("authentication updated") + getattr(self.on, "authentication_updated").emit( + relation, app=relation.app, unit=remote_unit + ) + + def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: + """Event emitted when the OpenSearch relation has changed. + + This event triggers individual custom events depending on the changing relation. + """ + # Check which data has changed to emit customs events. + diff = self._diff(event) + + # Register all new secrets with their labels + if any(newval for newval in diff.added if self.relation_data._is_secret_field(newval)): + self.relation_data._register_secrets_to_relation(event.relation, diff.added) + + secret_field_user = self.relation_data._generate_secret_field_name(SECRET_GROUPS.USER) + secret_field_tls = self.relation_data._generate_secret_field_name(SECRET_GROUPS.TLS) + updates = {"username", "password", "tls", "tls-ca", secret_field_user, secret_field_tls} + if len(set(diff._asdict().keys()) - updates) < len(diff): + logger.info("authentication updated at: %s", datetime.now()) + getattr(self.on, "authentication_updated").emit( + event.relation, app=event.app, unit=event.unit + ) + + # Check if the index is created + # (the OpenSearch charm shares the credentials). + if ( + "username" in diff.added and "password" in diff.added + ) or secret_field_user in diff.added: + # Emit the default event (the one without an alias). + logger.info("index created at: %s", datetime.now()) + getattr(self.on, "index_created").emit(event.relation, app=event.app, unit=event.unit) + + # To avoid unnecessary application restarts do not trigger + # “endpoints_changed“ event if “index_created“ is triggered. + return + + # Emit a endpoints changed event if the OpenSearch application added or changed this info + # in the relation databag. + if "endpoints" in diff.added or "endpoints" in diff.changed: + # Emit the default event (the one without an alias). + logger.info("endpoints changed on %s", datetime.now()) + getattr(self.on, "endpoints_changed").emit( + event.relation, app=event.app, unit=event.unit + ) # here check if this is the right design + return + + +class OpenSearchRequires(OpenSearchRequiresData, OpenSearchRequiresEventHandlers): + """Requires-side of the OpenSearch relation.""" + + def __init__( + self, + charm: CharmBase, + relation_name: str, + index: str, + extra_user_roles: Optional[str] = None, + additional_secret_fields: Optional[List[str]] = [], + ) -> None: + OpenSearchRequiresData.__init__( + self, + charm.model, + relation_name, + index, + extra_user_roles, + additional_secret_fields, + ) + OpenSearchRequiresEventHandlers.__init__(self, charm, self) diff --git a/charms/jimm-k8s/lib/charms/data_platform_libs/v0/database_requires.py b/charms/jimm-k8s/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 53d619128..000000000 --- a/charms/jimm-k8s/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,496 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# -# 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. - -"""Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import DatabaseRequires - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# Increment this major API version when introducing breaking changes -LIBAPI = 0 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version. -LIBPATCH = 4 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: str = None, - relations_aliases: List[str] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = { - key: value for key, value in event.relation.data[event.app].items() if key != "data" - } - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = { - key: value for key, value in relation.data[relation.app].items() if key != "data" - } - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - self.on.database_created.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - self.on.endpoints_changed.emit(event.relation, app=event.app, unit=event.unit) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - self.on.read_only_endpoints_changed.emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/charms/jimm-k8s/src/charm.py b/charms/jimm-k8s/src/charm.py index a07304a82..25a6592a7 100755 --- a/charms/jimm-k8s/src/charm.py +++ b/charms/jimm-k8s/src/charm.py @@ -20,9 +20,9 @@ from urllib.parse import urljoin, urlparse import requests -from charms.data_platform_libs.v0.database_requires import ( - DatabaseEvent, +from charms.data_platform_libs.v0.data_interfaces import ( DatabaseRequires, + DatabaseRequiresEvent, ) from charms.grafana_k8s.v0.grafana_dashboard import GrafanaDashboardProvider from charms.hydra.v0.oauth import ClientConfig, OAuthInfoChangedEvent, OAuthRequirer @@ -164,7 +164,6 @@ def __init__(self, *args): self.database.on.endpoints_changed, self._on_database_event, ) - self.framework.observe(self.on.database_relation_broken, self._on_database_relation_broken) # OpenFGA relation self.openfga = OpenFGARequires(self, OPENFGA_STORE_NAME) @@ -415,8 +414,11 @@ def _on_update_status(self, event): # update vault relation if exists binding = self.model.get_binding("vault-kv") if binding is not None: - egress_subnet = str(binding.network.interfaces[0].subnet) - self.interface.request_credentials(event.relation, egress_subnet, self.get_vault_nonce()) + try: + egress_subnet = str(binding.network.interfaces[0].subnet) + self.interface.request_credentials(event.relation, egress_subnet, self.get_vault_nonce()) + except Exception as e: + logging.warning(f"failed to update vault relation - {repr(e)}") @requires_state_setter def _on_dashboard_relation_joined(self, event: RelationJoinedEvent): @@ -432,7 +434,7 @@ def _on_dashboard_relation_joined(self, event: RelationJoinedEvent): ) @requires_state_setter - def _on_database_event(self, event: DatabaseEvent) -> None: + def _on_database_event(self, event: DatabaseRequiresEvent) -> None: """Database event handler.""" if event.username is None or event.password is None: @@ -455,17 +457,6 @@ def _on_database_event(self, event: DatabaseEvent) -> None: self._update_workload(event) - @requires_state_setter - def _on_database_relation_broken(self, event: DatabaseEvent) -> None: - """Database relation broken handler.""" - - # when the database relation is broken, we unset the - # connection string and schema-created from the application - # bucket of the peer relation - del self._state.dsn - - self._update_workload(event) - def _ready(self): container = self.unit.get_container(WORKLOAD_CONTAINER) @@ -508,14 +499,6 @@ def _on_vault_ready(self, event: vault_kv.VaultKvReadyEvent): def _on_vault_gone_away(self, event: vault_kv.VaultKvGoneAwayEvent): self._update_workload(event) - def _path_exists_in_workload(self, path: str): - """Returns true if the specified path exists in the - workload container.""" - container = self.unit.get_container(WORKLOAD_CONTAINER) - if container.can_connect(): - return container.exists(path) - return False - @requires_state_setter def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): if not event.store_id: @@ -559,7 +542,7 @@ def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: subject=dns_name, ) - self._state.csr = csr.decode() + self._state.csr = csr.decode().removesuffix("\n") self.certificates.request_certificate_creation(certificate_signing_request=csr) @@ -702,13 +685,5 @@ def ensureFQDN(dns: str): # noqa: N802 return dns -def _json_data(event, key): - logger.debug("getting relation data {}".format(key)) - try: - return json.loads(event.relation.data[event.unit][key]) - except KeyError: - return None - - if __name__ == "__main__": main(JimmOperatorCharm) diff --git a/charms/jimm-k8s/tests/unit/test_charm.py b/charms/jimm-k8s/tests/unit/test_charm.py index cdee15f83..bb67eafcc 100644 --- a/charms/jimm-k8s/tests/unit/test_charm.py +++ b/charms/jimm-k8s/tests/unit/test_charm.py @@ -7,10 +7,10 @@ import json import pathlib import tempfile -import unittest +from unittest import TestCase, mock -from ops.model import ActiveStatus, BlockedStatus -from ops.testing import Harness +from ops.model import ActiveStatus, BlockedStatus, WaitingStatus +from ops.testing import ActionFailed, Harness from src.charm import JimmOperatorCharm @@ -102,7 +102,7 @@ def wait_output(): return True -class TestCharm(unittest.TestCase): +class TestCharm(TestCase): def setUp(self): self.maxDiff = None self.harness = Harness(JimmOperatorCharm) @@ -181,7 +181,54 @@ def add_oauth_relation(self): }, ) - # import ipdb; ipdb.set_trace() + def add_postgres_relation(self): + self.postgres_rel_id = self.harness.add_relation("database", "postgresql") + self.harness.add_relation_unit(self.postgres_rel_id, "postgresql/0") + self.harness.update_relation_data( + self.postgres_rel_id, + "postgresql", + { + "username": "postgres-user", + "password": "postgres-pass", + "endpoints": "local-1.localhost,local-2.localhost", + }, + ) + + def start_minimal_jimm(self): + self.harness.enable_hooks() + self.harness.charm._state.dsn = "postgres-dsn" + self.add_openfga_relation() + self.add_vault_relation() + self.harness.charm._state.openfga_auth_model_id = 1 + self.harness.update_config(MINIMAL_CONFIG) + self.assertEqual(self.harness.charm.unit.status.name, ActiveStatus.name) + self.assertEqual(self.harness.charm.unit.status.message, "running") + + def test_add_certificates_relation(self): + self.start_minimal_jimm() + self.harness.set_leader(True) + self.certificates_rel_id = self.harness.add_relation("certificates", "certificates") + self.harness.add_relation_unit(self.certificates_rel_id, "certificates/0") + self.harness.update_relation_data( + self.certificates_rel_id, + "certificates", + { + "certificates": json.dumps( + [ + { + "certificate": "cert", + "ca": "ca", + "chain": ["chain"], + "certificate_signing_request": self.harness.charm._state.csr, + } + ] + ) + }, + ) + self.assertEqual(self.harness.charm._state.ca, "ca") + self.assertEqual(self.harness.charm._state.certificate, "cert") + self.assertEqual(self.harness.charm._state.chain, ["chain"]) + def test_on_pebble_ready(self): self.harness.enable_hooks() self.add_vault_relation() @@ -195,6 +242,12 @@ def test_on_pebble_ready(self): plan = self.harness.get_container_pebble_plan("jimm") self.assertEqual(plan.to_dict(), get_expected_plan(EXPECTED_VAULT_ENV)) + def test_ready_without_plan(self): + self.harness.enable_hooks() + self.harness.charm._ready() + self.assertEqual(self.harness.charm.unit.status.name, BlockedStatus.name) + self.assertEqual(self.harness.charm.unit.status.message, "Waiting for OAuth relation") + def test_on_config_changed(self): self.harness.enable_hooks() self.add_vault_relation() @@ -211,6 +264,25 @@ def test_on_config_changed(self): plan = self.harness.get_container_pebble_plan("jimm") self.assertEqual(plan.to_dict(), get_expected_plan(EXPECTED_VAULT_ENV)) + def test_stop(self): + self.start_minimal_jimm() + self.harness.charm.on.stop.emit() + self.assertEqual(self.harness.charm.unit.status.name, WaitingStatus.name) + self.assertEqual(self.harness.charm.unit.status.message, "stopped") + + def test_update_status(self): + self.start_minimal_jimm() + self.harness.charm.on.update_status.emit() + self.assertEqual(self.harness.charm.unit.status.name, ActiveStatus.name) + self.assertEqual(self.harness.charm.unit.status.message, "running") + + def test_postgres_relation_joined(self): + self.harness.enable_hooks() + self.add_postgres_relation() + self.assertEqual( + self.harness.charm._state.dsn, "postgresql://postgres-user:postgres-pass@local-1.localhost/jimm" + ) + def test_postgres_secret_storage_config(self): self.harness.update_config(MINIMAL_CONFIG) self.harness.update_config({"postgres-secret-storage": True}) @@ -337,3 +409,28 @@ def test_app_blocked_without_private_key(self): self.harness.update_config(MINIMAL_CONFIG) self.assertEqual(self.harness.charm.unit.status.name, ActiveStatus.name) self.assertEqual(self.harness.charm.unit.status.message, "running") + + @mock.patch("src.charm.requests.post") + def test_create_auth_model_action(self, mock_post): + def mocked_requests_post(*args, **kwargs): + class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + self.ok = True + + def json(self): + return self.json_data + + return MockResponse({"authorization_model_id": 123}, 200) + + mock_post.side_effect = mocked_requests_post + self.harness.enable_hooks() + self.add_openfga_relation() + self.harness.run_action("create-authorization-model", {"model": "null"}) + self.assertEqual(self.harness.charm._state.openfga_auth_model_id, 123) + + def test_create_auth_model_action_without_openfga_relation(self): + with self.assertRaises(ActionFailed) as e: + self.harness.run_action("create-authorization-model", {"model": "null"}) + self.assertEqual(str(e.exception.message), "missing openfga relation") From b64c63ef167f3300d3e4346085f2adec46bf8f78 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 2 May 2024 10:55:05 +0100 Subject: [PATCH 114/126] Update DNSs (#1203) Signed-off-by: Babak K. Shandiz --- local/traefik/certs/san.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local/traefik/certs/san.conf b/local/traefik/certs/san.conf index fa133773c..d3ebf9414 100644 --- a/local/traefik/certs/san.conf +++ b/local/traefik/certs/san.conf @@ -2,5 +2,5 @@ [v3_req] subjectKeyIdentifier = hash basicConstraints = critical,CA:false -subjectAltName = DNS:jimm.localhost,IP:127.0.0.1 +subjectAltName = DNS:jimm.localhost,IP:127.0.0.1,DNS:localhost,DNS:juju-apiserver,DNS:juju-mongodb keyUsage = critical,digitalSignature,keyEncipherment From 9bbfaf905eb6bf25fa95c5c1977c81c069daebb9 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 3 May 2024 11:38:07 +0200 Subject: [PATCH 115/126] CSS-8326 Update k8s charm readme (#1200) * Update README.md * Minor tweaks to README --- charms/jimm-k8s/README.md | 64 ++++++++++++--------------------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/charms/jimm-k8s/README.md b/charms/jimm-k8s/README.md index 11e5d2c0d..47c9e06c5 100644 --- a/charms/jimm-k8s/README.md +++ b/charms/jimm-k8s/README.md @@ -1,59 +1,33 @@ -# JAAS Intelligent Model Manager +# JIMM (K8s Charm) -## Description - -JIMM provides centralized model management for JAAS systems. +[![CharmHub Badge](https://charmhub.io/juju-jimm-k8s/badge.svg)](https://charmhub.io/juju-jimm-k8s) +[![Release](https://github.com/canonical/jimm/actions/workflows/charm-release.yaml/badge.svg)](https://github.com/canonical/jimm/actions/workflows/charm-release.yaml) +[![Tests](https://github.com/canonical/jimm/actions/workflows/charm-test.yaml/badge.svg?branch=v3)](https://github.com/canonical/jimm/actions/workflows/charm-test.yaml?query=branch%3Av3) -## Usage +## Description -The JIMM payload is provided by a JIMM snap that must be attached to -the application: +JIMM is a extension of Juju, an open source orchestration engine, providing additional capabilities to your Juju environments. +The JIMM K8s charm is the easiest and the recommended way to deploy JIMM. This charm installs and configures the JIMM server. -``` -juju deploy ./jimm-k8s.charm --resource jimm-image=jimm:latest -``` +JIMM provides a number of useful features on top of Juju including, +- A single location to manage your Juju infrastructure. +- The ability to query across multiple Juju controllers simultaneously. +- Expanded authentication and authorisation functionality utilising OAuth2.0 and Relationship-based Access Control (ReBAC). -To upgrade the workload attach a new version of the jimm container: +For users who want to deploy JIMM in its entirety (including its dependencies), it is recommended to visit [our documentation](https://canonical-jaas-documentation.readthedocs-hosted.com/en/latest/) for more details. -``` -juju attach jimm-k8s jimm-image=jimm:latest -``` +## Usage -JIMM requires a postgresql database for data storage: +JIMM can be deployed with the following command which will alias the deployed application name as simply `jimm`. ``` -juju jimm-k8s dsn='postgres://...' +juju deploy juju-jimm-k8s jimm ``` -## Developing - -Create and activate a virtualenv with the development requirements: +## Documentation - virtualenv -p python3 venv - source venv/bin/activate - pip install -r requirements-dev.txt +For more detailed instructions on deploying and using JIMM, please visit our [documentation page](https://canonical-jaas-documentation.readthedocs-hosted.com/en/latest/). -## Testing +## Contributing -The Python operator framework includes a very nice harness for testing -operator behaviour without full deployment. The test suite can be run -using `tox`. You can either `pip install tox` system-wide or create a -virtual env and install tox there as follows -``` -python3 -m venv venv -source ./venv/bin/activate -pip install tox -``` -At this point you can run tests/linters/formatters. -``` -tox -e fmt -tox -e lint -tox -e unit -tox -e integration -``` -Note that integration tests will build the charm and deploy it to a local -microk8s controller (which must be setup prior to running the integration test). -To switch the integration test to use a locally built charm use -``` -tox -e integration -- --localCharm -``` +Please see the [Juju SDK documentation](https://juju.is/docs/sdk) for more information about developing and improving charms. For developer guidance please check our contribution [guideline](CONTRIBUTING.md). From 3aff522ca296d392657bc41bbbe9294e30633b7e Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Tue, 7 May 2024 15:51:47 +0200 Subject: [PATCH 116/126] Add OCI image release action (#1207) --- .github/workflows/publich-oci.yaml | 33 ++++++++++++++++++++++++++++++ Dockerfile | 2 ++ 2 files changed, 35 insertions(+) create mode 100644 .github/workflows/publich-oci.yaml diff --git a/.github/workflows/publich-oci.yaml b/.github/workflows/publich-oci.yaml new file mode 100644 index 000000000..26c32f642 --- /dev/null +++ b/.github/workflows/publich-oci.yaml @@ -0,0 +1,33 @@ +# Publish the OCI image to ghcr +name: Publish image + +on: + # Note that when running via workflow_dispatch, the github.ref_name + # variable will match the selected branch name used. + workflow_dispatch: + push: + tags: + - 'v*' + +jobs: + publish: + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build local images + run: make jimm-image + + - name: Push to github package + run: | + new_tag=ghcr.io/canonical/jimm:${{ github.ref_name }} + docker tag jimm:latest $new_tag + docker push $new_tag diff --git a/Dockerfile b/Dockerfile index 4ec7c475f..99b71535f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,8 @@ RUN go build -tags version -o jimmsrv -v ./cmd/jimmsrv # Define a smaller single process image for deployment FROM ${DOCKER_REGISTRY}ubuntu:20.04 AS deploy-env +LABEL org.opencontainers.image.source=https://github.com/canonical/jimm +LABEL org.opencontainers.image.description="JIMM server container image" RUN apt-get -qq update && apt-get -qq install -y ca-certificates postgresql-client WORKDIR /root/ COPY --from=build-env /usr/src/jimm/jimmsrv . From 4dc51a61648a27aa1acdcd2f1335eaeeea434949 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Wed, 8 May 2024 10:43:08 +0200 Subject: [PATCH 117/126] Apply proxy login fix with Terraform (#1204) * Apply proxy login fix * Ran go mod tidy Also updated gitignore * Update proxy test * Revert Juju upgrade * Revert tweak to SAN * Revert other go.mod upgrades --- internal/rpc/proxy.go | 9 +++++++-- internal/rpc/proxy_test.go | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/rpc/proxy.go b/internal/rpc/proxy.go index 4357ccc24..90b257caf 100644 --- a/internal/rpc/proxy.go +++ b/internal/rpc/proxy.go @@ -20,6 +20,7 @@ import ( "github.com/canonical/jimm/internal/jimm/credentials" "github.com/canonical/jimm/internal/openfga" "github.com/canonical/jimm/internal/utils" + jimmnames "github.com/canonical/jimm/pkg/names" ) const ( @@ -643,12 +644,16 @@ func (p *clientProxy) handleAdminFacade(ctx context.Context, msg *message) (clie if err != nil { return errorFnc(err) } + clientIdWithDomain, err := jimmnames.EnsureValidServiceAccountId(request.ClientID) + if err != nil { + return errorFnc(err) + } err = p.jimm.OAuthAuthenticationService().VerifyClientCredentials(ctx, request.ClientID, request.ClientSecret) if err != nil { return errorFnc(err) } - user, err := p.jimm.GetOpenFGAUserAndAuthorise(ctx, request.ClientID) + user, err := p.jimm.GetOpenFGAUserAndAuthorise(ctx, clientIdWithDomain) if err != nil { return errorFnc(err) } @@ -658,7 +663,7 @@ func (p *clientProxy) handleAdminFacade(ctx context.Context, msg *message) (clie return errorFnc(err) } data, err := json.Marshal(params.LoginRequest{ - AuthTag: names.NewUserTag(request.ClientID).String(), + AuthTag: names.NewUserTag(clientIdWithDomain).String(), Token: base64.StdEncoding.EncodeToString(jwt), }) if err != nil { diff --git a/internal/rpc/proxy_test.go b/internal/rpc/proxy_test.go index 79430bbf8..619746c90 100644 --- a/internal/rpc/proxy_test.go +++ b/internal/rpc/proxy_test.go @@ -53,7 +53,7 @@ func TestProxySocketsAdminFacade(t *testing.T) { c.Assert(err, qt.IsNil) serviceAccountLoginData, err := json.Marshal(params.LoginRequest{ - AuthTag: names.NewUserTag("test-client-id").String(), + AuthTag: names.NewUserTag("test-client-id@serviceaccount").String(), Token: "dGVzdCB0b2tlbg==", }) c.Assert(err, qt.IsNil) From 04d81fa725c5ff77527747436c3add125af0cbbd Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Wed, 8 May 2024 11:20:52 +0200 Subject: [PATCH 118/126] CSS-8325 Jimm k8s charm integration tests (#1205) * WIP integration tests with identity bundle * Basic integration test working * Basic integration test * Working integration test * Refactored test setup * Test fixes * Update charm test workflow * Deleted 1 old test and marked other for skip * Update unit tests * Update upload/download artifact * Renamind and tweak to microk8s metallb IP * Workflow tweaks * Deploy identity bundle without fast forward * Tweak integration test setup * Fix auth code test --- .github/workflows/charm-build.yaml | 2 +- .github/workflows/charm-test.yaml | 63 +-- Makefile | 2 +- .../v0/certificate_transfer.py | 394 ++++++++++++++++++ charms/jimm-k8s/metadata.yaml | 7 + charms/jimm-k8s/pyproject.toml | 6 + charms/jimm-k8s/src/charm.py | 88 +++- .../jimm-k8s/tests/integration/test_charm.py | 125 ------ .../integration/test_charm_browser_login.py | 55 +++ .../integration/test_charm_with_nginx.py | 127 ------ .../tests/integration/test_upgrade.py | 3 + charms/jimm-k8s/tests/integration/utils.py | 137 +++++- charms/jimm-k8s/tests/unit/test_charm.py | 10 +- charms/jimm-k8s/tox.ini | 7 + cmd/jimmsrv/main.go | 1 + internal/auth/oauth2.go | 16 +- internal/auth/oauth2_test.go | 2 +- service.go | 1 + 18 files changed, 739 insertions(+), 307 deletions(-) create mode 100644 charms/jimm-k8s/lib/charms/certificate_transfer_interface/v0/certificate_transfer.py delete mode 100644 charms/jimm-k8s/tests/integration/test_charm.py create mode 100644 charms/jimm-k8s/tests/integration/test_charm_browser_login.py delete mode 100644 charms/jimm-k8s/tests/integration/test_charm_with_nginx.py diff --git a/.github/workflows/charm-build.yaml b/.github/workflows/charm-build.yaml index b356f5dcf..2d1391dbd 100644 --- a/.github/workflows/charm-build.yaml +++ b/.github/workflows/charm-build.yaml @@ -17,7 +17,7 @@ jobs: - run: git fetch --prune - run: sudo snap install charmcraft --channel=2.x/stable --classic - run: sudo charmcraft pack --project-dir ./charms/${{ matrix.charm-type }} --destructive-mode --verbosity=trace - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: ${{ matrix.charm-type }}-charm path: ./*.charm diff --git a/.github/workflows/charm-test.yaml b/.github/workflows/charm-test.yaml index d0d9b73a5..a6398c089 100644 --- a/.github/workflows/charm-test.yaml +++ b/.github/workflows/charm-test.yaml @@ -49,33 +49,36 @@ jobs: - name: Run tests run: tox -e unit - # TODO(Kian): Fix this - # integration-tests: - # name: Integration tests - # needs: - # - charm-build - # runs-on: ubuntu-latest - # env: - # charm-type: "jimm-k8s" - # steps: - # - name: Checkout - # uses: actions/checkout@v3 - # - name: Setup operator environment - # uses: charmed-kubernetes/actions-operator@main - # with: - # juju-channel: 2.9/stable - # provider: microk8s - # microk8s-addons: "ingress storage dns rbac registry" - # channel: 1.27/stable - # # Download the charm from the build to speed up integration tests. - # - uses: actions/download-artifact@master - # with: - # name: jimm-k8s-charm - # path: ./charms/${{ env.charm-type }} - # - name: Create OCI Image - # run: make push-microk8s - # - name: Install tox - # run: python -m pip install tox - # - name: Integration tests - # run: tox -e integration -- --localCharm - # working-directory: ./charms/${{ env.charm-type }} + integration-tests: + name: Integration tests + needs: + - charm-build + runs-on: ubuntu-latest + env: + charm-type: "jimm-k8s" + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@main + with: + channel: 1.28-strict/stable + juju-channel: 3.4/stable + provider: microk8s + microk8s-group: snap_microk8s + microk8s-addons: "ingress hostpath-storage dns registry metallb:10.64.140.43/30" + # Download the charm from the build to speed up integration tests. + - uses: actions/download-artifact@v4 + with: + name: jimm-k8s-charm + path: ./charms/${{ env.charm-type }} + - name: Create OCI Image + run: make push-microk8s + - name: Install tox + run: python -m pip install tox + - name: Integration tests + run: tox -e integration -- --localCharm + working-directory: ./charms/${{ env.charm-type }} + - name: Dump logs + if: failure() + uses: canonical/charming-actions/dump-logs@main diff --git a/Makefile b/Makefile index 5a617aad3..582360578 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ dev-env: @docker compose --profile dev up --force-recreate dev-env-cleanup: - @docker compose down -v --remove-orphans + @docker compose --profile dev down -v --remove-orphans # Reformat all source files. format: diff --git a/charms/jimm-k8s/lib/charms/certificate_transfer_interface/v0/certificate_transfer.py b/charms/jimm-k8s/lib/charms/certificate_transfer_interface/v0/certificate_transfer.py new file mode 100644 index 000000000..b07b83553 --- /dev/null +++ b/charms/jimm-k8s/lib/charms/certificate_transfer_interface/v0/certificate_transfer.py @@ -0,0 +1,394 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Library for the certificate_transfer relation. + +This library contains the Requires and Provides classes for handling the +ertificate-transfer interface. + +## Getting Started +From a charm directory, fetch the library using `charmcraft`: + +```shell +charmcraft fetch-lib charms.certificate_transfer_interface.v0.certificate_transfer +``` + +### Provider charm +The provider charm is the charm providing public certificates to another charm that requires them. + +Example: +```python +from ops.charm import CharmBase, RelationJoinedEvent +from ops.main import main + +from lib.charms.certificate_transfer_interface.v0.certificate_transfer import( + CertificateTransferProvides, +) + + +class DummyCertificateTransferProviderCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.certificate_transfer = CertificateTransferProvides(self, "certificates") + self.framework.observe( + self.on.certificates_relation_joined, self._on_certificates_relation_joined + ) + + def _on_certificates_relation_joined(self, event: RelationJoinedEvent): + certificate = "my certificate" + ca = "my CA certificate" + chain = ["certificate 1", "certificate 2"] + self.certificate_transfer.set_certificate( + certificate=certificate, ca=ca, chain=chain, relation_id=event.relation.id + ) + + +if __name__ == "__main__": + main(DummyCertificateTransferProviderCharm) +``` + +### Requirer charm +The requirer charm is the charm requiring certificates from another charm that provides them. + +Example: +```python + +from ops.charm import CharmBase +from ops.main import main + +from lib.charms.certificate_transfer_interface.v0.certificate_transfer import ( + CertificateAvailableEvent, + CertificateRemovedEvent, + CertificateTransferRequires, +) + + +class DummyCertificateTransferRequirerCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.certificate_transfer = CertificateTransferRequires(self, "certificates") + self.framework.observe( + self.certificate_transfer.on.certificate_available, self._on_certificate_available + ) + self.framework.observe( + self.certificate_transfer.on.certificate_removed, self._on_certificate_removed + ) + + def _on_certificate_available(self, event: CertificateAvailableEvent): + print(event.certificate) + print(event.ca) + print(event.chain) + print(event.relation_id) + + def _on_certificate_removed(self, event: CertificateRemovedEvent): + print(event.relation_id) + + +if __name__ == "__main__": + main(DummyCertificateTransferRequirerCharm) +``` + +You can relate both charms by running: + +```bash +juju relate +``` + +""" + + +import json +import logging +from typing import List, Mapping + +from jsonschema import exceptions, validate # type: ignore[import-untyped] +from ops.charm import CharmBase, CharmEvents, RelationBrokenEvent, RelationChangedEvent +from ops.framework import EventBase, EventSource, Handle, Object + +# The unique Charmhub library identifier, never change it +LIBID = "3785165b24a743f2b0c60de52db25c8b" + +# Increment this major API version when introducing breaking changes +LIBAPI = 0 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 7 + +PYDEPS = ["jsonschema"] + + +logger = logging.getLogger(__name__) + + +PROVIDER_JSON_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://canonical.github.io/charm-relation-interfaces/interfaces/certificate_transfer/schemas/provider.json", # noqa: E501 + "type": "object", + "title": "`certificate_transfer` provider schema", + "description": "The `certificate_transfer` root schema comprises the entire provider application databag for this interface.", # noqa: E501 + "default": {}, + "examples": [ + { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIC6DCCAdCgAwIBAgIUW42TU9LSjEZLMCclWrvSwAsgRtcwDQYJKoZIhvcNAQEL\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIzMDMyNDE4\nNDMxOVoXDTI0MDMyMzE4NDMxOVowPDELMAkGA1UEAwwCb2sxLTArBgNVBC0MJGUw\nNjVmMWI3LTE2OWEtNDE5YS1iNmQyLTc3OWJkOGM4NzIwNjCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAK42ixoklDH5K5i1NxXo/AFACDa956pE5RA57wlC\nBfgUYaIDRmv7TUVJh6zoMZSD6wjSZl3QgP7UTTZeHbvs3QE9HUwEkH1Lo3a8vD3z\neqsE2vSnOkpWWnPbfxiQyrTm77/LAWBt7lRLRLdfL6WcucD3wsGqm58sWXM3HG0f\nSN7PHCZUFqU6MpkHw8DiKmht5hBgWG+Vq3Zw8MNaqpwb/NgST3yYdcZwb58G2FTS\nZvDSdUfRmD/mY7TpciYV8EFylXNNFkth8oGNLunR9adgZ+9IunfRKj1a7S5GSwXU\nAZDaojw+8k5i3ikztsWH11wAVCiLj/3euIqq95z8xGycnKcCAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEAWMvcaozgBrZ/MAxzTJmp5gZyLxmMNV6iT9dcqbwzDtDtBvA/\n46ux6ytAQ+A7Bd3AubvozwCr1Id6g66ae0blWYRRZmF8fDdX/SBjIUkv7u9A3NVQ\nXN9gsEvK9pdpfN4ZiflfGSLdhM1STHycLmhG6H5s7HklbukMRhQi+ejbSzm/wiw1\nipcxuKhSUIVNkTLusN5b+HE2gwF1fn0K0z5jWABy08huLgbaEKXJEx5/FKLZGJga\nfpIzAdf25kMTu3gggseaAmzyX3AtT1i8A8nqYfe8fnnVMkvud89kq5jErv/hlMC9\n49g5yWQR2jilYYM3j9BHDuB+Rs+YS5BCep1JnQ==\n-----END CERTIFICATE-----\n", # noqa: E501 + "ca": "-----BEGIN CERTIFICATE-----\nMIIC6DCCAdCgAwIBAgIUdiBwE/CtaBXJl3MArjZen6Y8kigwDQYJKoZIhvcNAQEL\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIzMDMyNDE4\nNDg1OVoXDTI0MDMyMzE4NDg1OVowPDELMAkGA1UEAwwCb2sxLTArBgNVBC0MJDEw\nMDdjNDBhLWUwYzMtNDVlOS05YTAxLTVlYjY0NWQ0ZmEyZDCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANOnUl6JDlXpLMRr/PxgtfE/E5Yk6E/TkPkPL/Kk\ntUGjEi42XZDg9zn3U6cjTDYu+rfKY2jiitfsduW6DQIkEpz3AvbuCMbbgnFpcjsB\nYysLSMTmuz/AVPrfnea/tQTALcONCSy1VhAjGSr81ZRSMB4khl9StSauZrbkpJ1P\nshqkFSUyAi31mKrnXz0Es/v0Yi0FzAlgWrZ4u1Ld+Bo2Xz7oK4mHf7/93Jc+tEaM\nIqG6ocD0q8bjPp0tlSxftVADNUzWlZfM6fue5EXzOsKqyDrxYOSchfU9dNzKsaBX\nkxbHEeSUPJeYYj7aVPEfAs/tlUGsoXQvwWfRie8grp2BoLECAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEACZARBpHYH6Gr2a1ka0mCWfBmOZqfDVan9rsI5TCThoylmaXW\nquEiZ2LObI+5faPzxSBhr9TjJlQamsd4ywout7pHKN8ZGqrCMRJ1jJbUfobu1n2k\nUOsY4+jzV1IRBXJzj64fLal4QhUNv341lAer6Vz3cAyRk7CK89b/DEY0x+jVpyZT\n1osx9JtsOmkDTgvdStGzq5kPKWOfjwHkmKQaZXliCgqbhzcCERppp1s/sX6K7nIh\n4lWiEmzUSD3Hngk51KGWlpZszO5KQ4cSZ3HUt/prg+tt0ROC3pY61k+m5dDUa9M8\nRtMI6iTjzSj/UV8DiAx0yeM+bKoy4jGeXmaL3g==\n-----END CERTIFICATE-----\n", # noqa: E501 + "chain": [ + "-----BEGIN CERTIFICATE-----\nMIIC6DCCAdCgAwIBAgIUW42TU9LSjEZLMCclWrvSwAsgRtcwDQYJKoZIhvcNAQEL\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIzMDMyNDE4\nNDMxOVoXDTI0MDMyMzE4NDMxOVowPDELMAkGA1UEAwwCb2sxLTArBgNVBC0MJGUw\nNjVmMWI3LTE2OWEtNDE5YS1iNmQyLTc3OWJkOGM4NzIwNjCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAK42ixoklDH5K5i1NxXo/AFACDa956pE5RA57wlC\nBfgUYaIDRmv7TUVJh6zoMZSD6wjSZl3QgP7UTTZeHbvs3QE9HUwEkH1Lo3a8vD3z\neqsE2vSnOkpWWnPbfxiQyrTm77/LAWBt7lRLRLdfL6WcucD3wsGqm58sWXM3HG0f\nSN7PHCZUFqU6MpkHw8DiKmht5hBgWG+Vq3Zw8MNaqpwb/NgST3yYdcZwb58G2FTS\nZvDSdUfRmD/mY7TpciYV8EFylXNNFkth8oGNLunR9adgZ+9IunfRKj1a7S5GSwXU\nAZDaojw+8k5i3ikztsWH11wAVCiLj/3euIqq95z8xGycnKcCAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEAWMvcaozgBrZ/MAxzTJmp5gZyLxmMNV6iT9dcqbwzDtDtBvA/\n46ux6ytAQ+A7Bd3AubvozwCr1Id6g66ae0blWYRRZmF8fDdX/SBjIUkv7u9A3NVQ\nXN9gsEvK9pdpfN4ZiflfGSLdhM1STHycLmhG6H5s7HklbukMRhQi+ejbSzm/wiw1\nipcxuKhSUIVNkTLusN5b+HE2gwF1fn0K0z5jWABy08huLgbaEKXJEx5/FKLZGJga\nfpIzAdf25kMTu3gggseaAmzyX3AtT1i8A8nqYfe8fnnVMkvud89kq5jErv/hlMC9\n49g5yWQR2jilYYM3j9BHDuB+Rs+YS5BCep1JnQ==\n-----END CERTIFICATE-----\n", # noqa: E501 + "-----BEGIN CERTIFICATE-----\nMIIC6DCCAdCgAwIBAgIUdiBwE/CtaBXJl3MArjZen6Y8kigwDQYJKoZIhvcNAQEL\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIzMDMyNDE4\nNDg1OVoXDTI0MDMyMzE4NDg1OVowPDELMAkGA1UEAwwCb2sxLTArBgNVBC0MJDEw\nMDdjNDBhLWUwYzMtNDVlOS05YTAxLTVlYjY0NWQ0ZmEyZDCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANOnUl6JDlXpLMRr/PxgtfE/E5Yk6E/TkPkPL/Kk\ntUGjEi42XZDg9zn3U6cjTDYu+rfKY2jiitfsduW6DQIkEpz3AvbuCMbbgnFpcjsB\nYysLSMTmuz/AVPrfnea/tQTALcONCSy1VhAjGSr81ZRSMB4khl9StSauZrbkpJ1P\nshqkFSUyAi31mKrnXz0Es/v0Yi0FzAlgWrZ4u1Ld+Bo2Xz7oK4mHf7/93Jc+tEaM\nIqG6ocD0q8bjPp0tlSxftVADNUzWlZfM6fue5EXzOsKqyDrxYOSchfU9dNzKsaBX\nkxbHEeSUPJeYYj7aVPEfAs/tlUGsoXQvwWfRie8grp2BoLECAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEACZARBpHYH6Gr2a1ka0mCWfBmOZqfDVan9rsI5TCThoylmaXW\nquEiZ2LObI+5faPzxSBhr9TjJlQamsd4ywout7pHKN8ZGqrCMRJ1jJbUfobu1n2k\nUOsY4+jzV1IRBXJzj64fLal4QhUNv341lAer6Vz3cAyRk7CK89b/DEY0x+jVpyZT\n1osx9JtsOmkDTgvdStGzq5kPKWOfjwHkmKQaZXliCgqbhzcCERppp1s/sX6K7nIh\n4lWiEmzUSD3Hngk51KGWlpZszO5KQ4cSZ3HUt/prg+tt0ROC3pY61k+m5dDUa9M8\nRtMI6iTjzSj/UV8DiAx0yeM+bKoy4jGeXmaL3g==\n-----END CERTIFICATE-----\n", # noqa: E501 + ], + } + ], + "properties": { + "certificate": { + "$id": "#/properties/certificate", + "type": "string", + "title": "Public TLS certificate", + "description": "Public TLS certificate", + }, + "ca": { + "$id": "#/properties/ca", + "type": "string", + "title": "CA public TLS certificate", + "description": "CA Public TLS certificate", + }, + "chain": { + "$id": "#/properties/chain", + "type": "array", + "items": {"type": "string", "$id": "#/properties/chain/items"}, + "title": "CA public TLS certificate chain", + "description": "CA public TLS certificate chain", + }, + }, + "anyOf": [{"required": ["certificate"]}, {"required": ["ca"]}, {"required": ["chain"]}], + "additionalProperties": True, +} + + +class CertificateAvailableEvent(EventBase): + """Charm Event triggered when a TLS certificate is available.""" + + def __init__( + self, + handle: Handle, + certificate: str, + ca: str, + chain: List[str], + relation_id: int, + ): + super().__init__(handle) + self.certificate = certificate + self.ca = ca + self.chain = chain + self.relation_id = relation_id + + def snapshot(self) -> dict: + """Return snapshot.""" + return { + "certificate": self.certificate, + "ca": self.ca, + "chain": self.chain, + "relation_id": self.relation_id, + } + + def restore(self, snapshot: dict): + """Restores snapshot.""" + self.certificate = snapshot["certificate"] + self.ca = snapshot["ca"] + self.chain = snapshot["chain"] + self.relation_id = snapshot["relation_id"] + + +class CertificateRemovedEvent(EventBase): + """Charm Event triggered when a TLS certificate is removed.""" + + def __init__(self, handle: Handle, relation_id: int): + super().__init__(handle) + self.relation_id = relation_id + + def snapshot(self) -> dict: + """Return snapshot.""" + return {"relation_id": self.relation_id} + + def restore(self, snapshot: dict): + """Restores snapshot.""" + self.relation_id = snapshot["relation_id"] + + +def _load_relation_data(raw_relation_data: Mapping[str, str]) -> dict: + """Load relation data from the relation data bag. + + Args: + raw_relation_data: Relation data from the databag + + Returns: + dict: Relation data in dict format. + """ + loaded_relation_data = {} + for key in raw_relation_data: + try: + loaded_relation_data[key] = json.loads(raw_relation_data[key]) + except (json.decoder.JSONDecodeError, TypeError): + loaded_relation_data[key] = raw_relation_data[key] + return loaded_relation_data + + +class CertificateTransferRequirerCharmEvents(CharmEvents): + """List of events that the Certificate Transfer requirer charm can leverage.""" + + certificate_available = EventSource(CertificateAvailableEvent) + certificate_removed = EventSource(CertificateRemovedEvent) + + +class CertificateTransferProvides(Object): + """Certificate Transfer provider class.""" + + def __init__(self, charm: CharmBase, relationship_name: str): + super().__init__(charm, relationship_name) + self.charm = charm + self.relationship_name = relationship_name + + def set_certificate( + self, + certificate: str, + ca: str, + chain: List[str], + relation_id: int, + ) -> None: + """Add certificates to relation data. + + Args: + certificate (str): Certificate + ca (str): CA Certificate + chain (list): CA Chain + relation_id (int): Juju relation ID + + Returns: + None + """ + relation = self.model.get_relation( + relation_name=self.relationship_name, + relation_id=relation_id, + ) + if not relation: + raise RuntimeError( + f"No relation found with relation name {self.relationship_name} and " + f"relation ID {relation_id}" + ) + relation.data[self.model.unit]["certificate"] = certificate + relation.data[self.model.unit]["ca"] = ca + relation.data[self.model.unit]["chain"] = json.dumps(chain) + + def remove_certificate(self, relation_id: int) -> None: + """Remove a given certificate from relation data. + + Args: + relation_id (int): Relation ID + + Returns: + None + """ + relation = self.model.get_relation( + relation_name=self.relationship_name, + relation_id=relation_id, + ) + if not relation: + logger.warning( + f"Can't remove certificate - Non-existent relation '{self.relationship_name}'" + ) + return + unit_relation_data = relation.data[self.model.unit] + certificate_removed = False + if "certificate" in unit_relation_data: + relation.data[self.model.unit].pop("certificate") + certificate_removed = True + if "ca" in unit_relation_data: + relation.data[self.model.unit].pop("ca") + certificate_removed = True + if "chain" in unit_relation_data: + relation.data[self.model.unit].pop("chain") + certificate_removed = True + + if certificate_removed: + logger.warning("Certificate removed from relation data") + else: + logger.warning("Can't remove certificate - No certificate in relation data") + + +class CertificateTransferRequires(Object): + """TLS certificates requirer class to be instantiated by TLS certificates requirers.""" + + on = CertificateTransferRequirerCharmEvents() # type: ignore + + def __init__( + self, + charm: CharmBase, + relationship_name: str, + ): + """Generates/use private key and observes relation changed event. + + Args: + charm: Charm object + relationship_name: Juju relation name + """ + super().__init__(charm, relationship_name) + self.relationship_name = relationship_name + self.charm = charm + self.framework.observe( + charm.on[relationship_name].relation_changed, self._on_relation_changed + ) + self.framework.observe( + charm.on[relationship_name].relation_broken, self._on_relation_broken + ) + + @staticmethod + def _relation_data_is_valid(relation_data: dict) -> bool: + """Return whether relation data is valid based on json schema. + + Args: + relation_data: Relation data in dict format. + + Returns: + bool: Whether relation data is valid. + """ + try: + validate(instance=relation_data, schema=PROVIDER_JSON_SCHEMA) + return True + except exceptions.ValidationError: + return False + + def _on_relation_changed(self, event: RelationChangedEvent) -> None: + """Emit certificate available event. + + Args: + event: Juju event + + Returns: + None + """ + if not event.unit: + logger.info(f"No remote unit in relation: {self.relationship_name}") + return + remote_unit_relation_data = _load_relation_data(event.relation.data[event.unit]) + if not self._relation_data_is_valid(remote_unit_relation_data): + logger.warning( + f"Provider relation data did not pass JSON Schema validation: " + f"{event.relation.data[event.unit]}" + ) + return + self.on.certificate_available.emit( + certificate=remote_unit_relation_data.get("certificate"), + ca=remote_unit_relation_data.get("ca"), + chain=remote_unit_relation_data.get("chain"), + relation_id=event.relation.id, + ) + + def _on_relation_broken(self, event: RelationBrokenEvent) -> None: + """Handle relation broken event. + + Args: + event: Juju event + + Returns: + None + """ + self.on.certificate_removed.emit(relation_id=event.relation.id) diff --git a/charms/jimm-k8s/metadata.yaml b/charms/jimm-k8s/metadata.yaml index 36c054123..0904a3eec 100644 --- a/charms/jimm-k8s/metadata.yaml +++ b/charms/jimm-k8s/metadata.yaml @@ -62,6 +62,13 @@ requires: oauth: interface: oauth limit: 1 + receive-ca-cert: + interface: certificate_transfer + description: | + Receive a CA cert for grafana to trust. + This relation can be used with a local CA to obtain the CA cert that was used to sign proxied + endpoints. + limit: 1 containers: jimm: diff --git a/charms/jimm-k8s/pyproject.toml b/charms/jimm-k8s/pyproject.toml index 85c3b0738..f364e3401 100644 --- a/charms/jimm-k8s/pyproject.toml +++ b/charms/jimm-k8s/pyproject.toml @@ -3,3 +3,9 @@ line-length = 120 [tool.isort] profile = "black" + +[tool.coverage.run] +branch = true + +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/charms/jimm-k8s/src/charm.py b/charms/jimm-k8s/src/charm.py index 25a6592a7..799be02d2 100755 --- a/charms/jimm-k8s/src/charm.py +++ b/charms/jimm-k8s/src/charm.py @@ -17,9 +17,14 @@ import json import logging import secrets +import string from urllib.parse import urljoin, urlparse import requests +from charms.certificate_transfer_interface.v0.certificate_transfer import ( + CertificateRemovedEvent, + CertificateTransferRequires, +) from charms.data_platform_libs.v0.data_interfaces import ( DatabaseRequires, DatabaseRequiresEvent, @@ -49,6 +54,7 @@ from ops.model import ( ActiveStatus, BlockedStatus, + Container, ErrorStatus, TooManyRelatedAppsError, WaitingStatus, @@ -80,10 +86,12 @@ # This likely will just be JIMM's port. PROMETHEUS_PORT = 8080 OAUTH = "oauth" -OAUTH_SCOPES = "openid email offline_access" +OAUTH_SCOPES = "openid profile email offline_access" # TODO: Add "device_code" below once the charm interface supports it. OAUTH_GRANT_TYPES = ["authorization_code", "refresh_token"] VAULT_NONCE_SECRET_LABEL = "nonce" +# Template for storing trusted certificate in a file. +TRUSTED_CA_TEMPLATE = string.Template("/usr/local/share/ca-certificates/trusted-ca-cert-$rel_id-ca.crt") class DeferError(Exception): @@ -203,6 +211,16 @@ def __init__(self, *args): self._on_create_authorization_model_action, ) + self.trusted_cert_transfer = CertificateTransferRequires(self, "receive-ca-cert") + self.framework.observe( + self.trusted_cert_transfer.on.certificate_available, + self._on_trusted_certificate_available, # pyright: ignore + ) + self.framework.observe( + self.trusted_cert_transfer.on.certificate_removed, + self._on_trusted_certificate_removed, # pyright: ignore + ) + def _on_peer_relation_changed(self, event): self._update_workload(event) @@ -304,10 +322,10 @@ def _update_workload(self, event): "JIMM_OAUTH_ISSUER_URL": oauth_provider_info.issuer_url, "JIMM_OAUTH_CLIENT_ID": oauth_provider_info.client_id, "JIMM_OAUTH_CLIENT_SECRET": oauth_provider_info.client_secret, - "JIMM_OAUTH_SCOPES": oauth_provider_info.scope, - "JIMM_DASHBOARD_FINAL_REDIRECT_URL:": self.config.get("final-redirect-url"), - "JIMM_SECURE_SESSION_COOKIES:": self.config.get("secure-session-cookies"), - "JIMM_SESSION_COOKIE_MAX_AGE:": self.config.get("session-cookie-max-age"), + "JIMM_OAUTH_SCOPES": OAUTH_SCOPES, + "JIMM_DASHBOARD_FINAL_REDIRECT_URL": self.config.get("final-redirect-url"), + "JIMM_SECURE_SESSION_COOKIES": self.config.get("secure-session-cookies"), + "JIMM_SESSION_COOKIE_MAX_AGE": self.config.get("session-cookie-max-age"), } if self._state.dsn: config_values["JIMM_DSN"] = self._state.dsn @@ -351,12 +369,17 @@ def _update_workload(self, event): } }, } + force_restart = self._update_trusted_ca_certs(container) container.add_layer("jimm", pebble_layer, combine=True) try: if self._ready(): if container.get_service(JIMM_SERVICE_NAME).is_running(): - logger.info("replanning service") - container.replan() + if force_restart: + logger.info("performing service restart") + container.restart(JIMM_SERVICE_NAME) + else: + logger.info("replanning service") + container.replan() else: logger.info("starting service") container.start(JIMM_SERVICE_NAME) @@ -667,7 +690,7 @@ def _oauth_client_config(self) -> ClientConfig: dns = "http://localhost" dns = ensureFQDN(dns) return ClientConfig( - urljoin(dns, "/oauth/callback"), + urljoin(dns, "/auth/callback"), OAUTH_SCOPES, OAUTH_GRANT_TYPES, ) @@ -677,6 +700,55 @@ def get_vault_nonce(self): nonce = secret.get_content()["nonce"] return nonce + def _update_trusted_ca_certs(self, container: Container) -> bool: + """This function receives the trusted certificates from the certificate_transfer integration. + + JIMM needs to restart to use newly received certificates. Certificates attached to the + relation need to be pulled before JIMM is started. + This function is needed because relation events are not emitted on upgrade, and because we + do not have (nor do we want) persistent storage for certs. + + Args: + container (Container): The workload container, the caller must ensure that we can connect. + + Returns: + bool: A boolean to indicate whether the workload service should be restarted. + """ + if not self.model.get_relation(relation_name=self.trusted_cert_transfer.relationship_name): + return False + + logger.info( + "Pulling trusted ca certificates from %s relation.", + self.trusted_cert_transfer.relationship_name, + ) + for relation in self.model.relations.get(self.trusted_cert_transfer.relationship_name, []): + # For some reason, relation.units includes our unit and app. Need to exclude them. + for unit in set(relation.units).difference([self.app, self.unit]): + # Note: this nested loop handles the case of multi-unit CA, each unit providing + # a different ca cert, but that is not currently supported by the lib itself. + cert_path = TRUSTED_CA_TEMPLATE.substitute(rel_id=relation.id) + if cert := relation.data[unit].get("ca"): + container.push(cert_path, cert, make_dirs=True) + + stdout, stderr = container.exec(["update-ca-certificates", "--fresh"]).wait_output() + logger.info("stdout update-ca-certificates: %s", stdout) + logger.info("stderr update-ca-certificates: %s", stderr) + + return True + + def _on_trusted_certificate_available(self, event): + self._update_workload(event) + + def _on_trusted_certificate_removed(self, event: CertificateRemovedEvent): + # All certificates received from the relation are in separate files marked by the relation id. + container = self.unit.get_container(WORKLOAD_CONTAINER) + if not container.can_connect(): + event.defer() + return + cert_path = TRUSTED_CA_TEMPLATE.substitute(rel_id=event.relation_id) + container.remove_path(cert_path, recursive=True) + self._update_workload(event) + def ensureFQDN(dns: str): # noqa: N802 """Ensures a domain name has an https:// prefix.""" diff --git a/charms/jimm-k8s/tests/integration/test_charm.py b/charms/jimm-k8s/tests/integration/test_charm.py deleted file mode 100644 index 300cb31b2..000000000 --- a/charms/jimm-k8s/tests/integration/test_charm.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2022 Canonical Ltd -# See LICENSE file for licensing details. - -import asyncio -import logging -import time -from pathlib import Path - -import pytest -import utils -import yaml -from juju.action import Action -from pytest_operator.plugin import OpsTest - -logger = logging.getLogger(__name__) - -METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) -APP_NAME = "juju-jimm-k8s" - - -@pytest.mark.abort_on_fail -async def test_build_and_deploy(ops_test: OpsTest, local_charm): - """Build the charm-under-test and deploy it together with related charms. - - Assert on the unit status before any relations/configurations take place. - """ - # (Optionally build) and deploy charm from local source folder - if local_charm: - charm = Path(utils.get_local_charm()).resolve() - else: - charm = await ops_test.build_charm(".") - resources = {"jimm-image": "localhost:32000/jimm:latest"} - - # Deploy the charm and wait for active/idle status - logger.debug("deploying charms") - await ops_test.model.deploy( - charm, - resources=resources, - application_name=APP_NAME, - series="focal", - config={ - "uuid": "f4dec11e-e2b6-40bb-871a-cc38e958af49", - "candid-url": "https://api.jujucharms.com/identity", - "public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", - "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", - "dns-name": "jimm.test.canonical.com", - }, - ) - await ops_test.model.deploy( - "traefik-k8s", - application_name="traefik", - config={ - "external_hostname": "traefik.test.canonical.com", - }, - ) - await asyncio.gather( - ops_test.model.deploy("postgresql-k8s", application_name="postgresql", channel="14/stable", trust=True), - ops_test.model.deploy( - "openfga-k8s", - application_name="openfga", - channel="latest/edge", - ), - ) - - logger.info("waiting for postgresql and traefik") - await ops_test.model.wait_for_idle( - apps=["postgresql", "traefik"], - status="active", - raise_on_blocked=True, - timeout=40000, - ) - - logger.info("adding traefik relation") - await ops_test.model.integrate("{}:ingress".format(APP_NAME), "traefik") - - logger.info("adding openfga postgresql relation") - await ops_test.model.integrate("openfga:database", "postgresql:database") - - logger.info("waiting for openfga") - await ops_test.model.wait_for_idle( - apps=["openfga"], - status="blocked", - timeout=40000, - ) - - openfga_unit = await utils.get_unit_by_name("openfga", "0", ops_test.model.units) - for i in range(10): - action: Action = await openfga_unit.run_action("schema-upgrade") - result = await action.wait() - logger.info("attempt {} -> action result {} {}".format(i, result.status, result.results)) - if result.results == {"result": "done", "return-code": 0}: - break - time.sleep(2) - - logger.info("adding openfga relation") - await ops_test.model.integrate(APP_NAME, "openfga") - - logger.info("adding postgresql relation") - await ops_test.model.integrate(APP_NAME, "postgresql:database") - - logger.info("waiting for jimm") - await ops_test.model.wait_for_idle( - apps=[APP_NAME], - status="active", - # raise_on_blocked=True, - timeout=40000, - ) - - logger.info("running the create authorization model action") - jimm_unit = await utils.get_unit_by_name(APP_NAME, "0", ops_test.model.units) - with open("../../local/openfga/authorisation_model.json", "r") as model_file: - model_data = model_file.read() - for i in range(10): - action: Action = await jimm_unit.run_action( - "create-authorization-model", - model=model_data, - ) - result = await action.wait() - logger.info("attempt {} -> action result {} {}".format(i, result.status, result.results)) - if result.results == {"return-code": 0}: - break - time.sleep(2) - - assert ops_test.model.applications[APP_NAME].status == "active" diff --git a/charms/jimm-k8s/tests/integration/test_charm_browser_login.py b/charms/jimm-k8s/tests/integration/test_charm_browser_login.py new file mode 100644 index 000000000..e455304eb --- /dev/null +++ b/charms/jimm-k8s/tests/integration/test_charm_browser_login.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# Copyright 2022 Canonical Ltd +# See LICENSE file for licensing details. + +import logging +import os + +import pytest +import requests +from oauth_tools.conftest import * # noqa +from oauth_tools.constants import EXTERNAL_USER_EMAIL +from oauth_tools.oauth_test_helper import ( + access_application_login_page, + complete_external_idp_login, + get_cookie_from_browser_by_name, + verify_page_loads, +) +from playwright.async_api._generated import BrowserContext, Page +from pytest_operator.plugin import OpsTest +from utils import deploy_jimm + +logger = logging.getLogger(__name__) + + +@pytest.mark.abort_on_fail +async def test_jimm_oauth_browser_login(ops_test: OpsTest, local_charm, page: Page, context: BrowserContext): + """Build the charm-under-test and deploy it together with related charms. + + Run a playwright test to perform the browser login flow and confirm the session cookie is valid. + """ + # Build and deploy charm from local source folder + # (Optionally build) and deploy charm from local source folder + jimm_env = await deploy_jimm(ops_test, local_charm) + logger.info("running browser flow login test") + logger.info(f"jimm's address is {jimm_env.jimm_address.geturl()}") + jimm_login_page = os.path.join(jimm_env.jimm_address.geturl(), "auth/login") + + await access_application_login_page(page=page, url=jimm_login_page) + logger.info("completing external idp login") + await complete_external_idp_login(page=page, ops_test=ops_test, external_idp_manager=jimm_env.idp_manager) + redirect_url = os.path.join(jimm_env.jimm_address.geturl(), "debug/info") + logger.info(f"verifying return to JIMM - expecting a final redirect to {redirect_url}") + await verify_page_loads(page=page, url=redirect_url) + + logger.info("verifying session cookie") + # Verifying that the login flow was successful is application specific. + # The test uses JIMM's /auth/whoami endpoint to verify the session cookie is valid + jimm_session_cookie = await get_cookie_from_browser_by_name(browser_context=context, name="jimm-browser-session") + request = requests.get( + os.path.join(jimm_env.jimm_address.geturl(), "auth/whoami"), + headers={"Cookie": f"jimm-browser-session={jimm_session_cookie}"}, + verify=False, + ) + assert request.status_code == 200 + assert request.json()["email"] == EXTERNAL_USER_EMAIL diff --git a/charms/jimm-k8s/tests/integration/test_charm_with_nginx.py b/charms/jimm-k8s/tests/integration/test_charm_with_nginx.py deleted file mode 100644 index 79d5a69f6..000000000 --- a/charms/jimm-k8s/tests/integration/test_charm_with_nginx.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2022 Canonical Ltd -# See LICENSE file for licensing details. - -import asyncio -import logging -import time -from pathlib import Path - -import pytest -import utils -import yaml -from juju.action import Action -from pytest_operator.plugin import OpsTest - -logger = logging.getLogger(__name__) - -METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) -APP_NAME = "juju-jimm-k8s" - - -@pytest.mark.abort_on_fail -async def test_build_and_deploy_with_ngingx(ops_test: OpsTest, local_charm): - """Build the charm-under-test and deploy it together with related charms. - - Assert on the unit status before any relations/configurations take place. - """ - # Build and deploy charm from local source folder - # (Optionally build) and deploy charm from local source folder - if local_charm: - charm = Path(utils.get_local_charm()).resolve() - else: - charm = await ops_test.build_charm(".") - resources = {"jimm-image": "localhost:32000/jimm:latest"} - - # Deploy the charm and wait for active/idle status - logger.debug("deploying charms") - await ops_test.model.deploy( - charm, - resources=resources, - application_name=APP_NAME, - series="focal", - config={ - "uuid": "f4dec11e-e2b6-40bb-871a-cc38e958af49", - "dns-name": "test.jimm.local", - "candid-url": "https://api.jujucharms.com/identity", - "public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", - "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", - }, - ) - await ops_test.model.deploy( - "nginx-ingress-integrator", - application_name="nginx", - ) - await asyncio.gather( - ops_test.model.deploy( - "postgresql-k8s", - application_name="postgresql", - channel="edge", - ), - ops_test.model.deploy( - "openfga-k8s", - application_name="openfga", - channel="edge", - ), - ) - - logger.info("waiting for postgresql") - await ops_test.model.wait_for_idle( - apps=["postgresql", "nginx"], - status="active", - raise_on_blocked=True, - timeout=40000, - ) - - logger.info("adding ingress relation") - await ops_test.model.relate("{}:nginx-route".format(APP_NAME), "nginx") - - logger.info("adding openfga postgresql relation") - await ops_test.model.relate("openfga:database", "postgresql:database") - - logger.info("waiting for openfga") - await ops_test.model.wait_for_idle( - apps=["openfga"], - status="blocked", - timeout=40000, - ) - - openfga_unit = await utils.get_unit_by_name("openfga", "0", ops_test.model.units) - for i in range(10): - action: Action = await openfga_unit.run_action("schema-upgrade") - result = await action.wait() - logger.info("attempt {} -> action result {} {}".format(i, result.status, result.results)) - if result.results == {"result": "done", "return-code": 0}: - break - time.sleep(2) - - logger.info("adding openfga relation") - await ops_test.model.relate(APP_NAME, "openfga") - - logger.info("adding postgresql relation") - await ops_test.model.relate(APP_NAME, "postgresql:database") - - logger.info("waiting for jimm") - await ops_test.model.wait_for_idle( - apps=[APP_NAME], - status="active", - # raise_on_blocked=True, - timeout=40000, - ) - - logger.info("running the create authorization model action") - jimm_unit = await utils.get_unit_by_name(APP_NAME, "0", ops_test.model.units) - with open("../../local/openfga/authorisation_model.json", "r") as model_file: - model_data = model_file.read() - for i in range(10): - action: Action = await jimm_unit.run_action( - "create-authorization-model", - model=model_data, - ) - result = await action.wait() - logger.info("attempt {} -> action result {} {}".format(i, result.status, result.results)) - if result.results == {"return-code": 0}: - break - time.sleep(2) - - assert ops_test.model.applications[APP_NAME].status == "active" diff --git a/charms/jimm-k8s/tests/integration/test_upgrade.py b/charms/jimm-k8s/tests/integration/test_upgrade.py index 29229e61a..252010582 100644 --- a/charms/jimm-k8s/tests/integration/test_upgrade.py +++ b/charms/jimm-k8s/tests/integration/test_upgrade.py @@ -19,7 +19,10 @@ APP_NAME = "juju-jimm-k8s" +# TODO: Update this test to use utils.deploy_jimm() and extend that function to allow the caller +# to decide where to deploy JIMM from. Then this test can just upgrade JIMM. @pytest.mark.abort_on_fail +@pytest.mark.skip(reason="todo: refactor things for this test.") async def test_upgrade_running_application(ops_test: OpsTest, local_charm): """Deploy latest published charm and upgrade it with charm-under-test. diff --git a/charms/jimm-k8s/tests/integration/utils.py b/charms/jimm-k8s/tests/integration/utils.py index aad7079cd..5876a818e 100644 --- a/charms/jimm-k8s/tests/integration/utils.py +++ b/charms/jimm-k8s/tests/integration/utils.py @@ -1,10 +1,26 @@ +import asyncio import glob import logging +import os +import time +from pathlib import Path from typing import Dict +from urllib.parse import ParseResult +import requests +import utils +import yaml +from juju.action import Action from juju.unit import Unit +from oauth_tools.conftest import * # noqa +from oauth_tools.constants import APPS +from oauth_tools.dex import ExternalIdpManager +from oauth_tools.oauth_test_helper import deploy_identity_bundle +from pytest_operator.plugin import OpsTest -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) +METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) +APP_NAME = "juju-jimm-k8s" async def get_unit_by_name(unit_name: str, unit_index: str, unit_list: Dict[str, Unit]) -> Unit: @@ -16,3 +32,122 @@ def get_local_charm(): if len(charm) != 1: raise ValueError(f"Found {len(charm)} file(s) with .charm extension.") return charm[0] + + +class JimmEnv: + def __init__(self, jimm_address: ParseResult, idp_manager: ExternalIdpManager) -> None: + self.jimm_address = jimm_address + self.idp_manager = idp_manager + + +async def deploy_jimm(ops_test: OpsTest, local_charm: bool) -> JimmEnv: + """(Optionally) Build and then deploy JIMM and all dependencies. + + Args: + ops_test (OpsTest): Fixture for testing operator charms + local_charm (bool): Flag to indicate whether to build the charm under test. + + Returns: + JimmEnv: A class with member variables that are useful for test functions. + """ + # Build and deploy charm from local source folder + # (Optionally build) and deploy charm from local source folder + if local_charm: + charm = Path(utils.get_local_charm()).resolve() + else: + charm = await ops_test.build_charm(".") + resources = {"jimm-image": "localhost:32000/jimm:latest"} + + jimm_address = ParseResult(scheme="http", netloc="test.jimm.localhost", path="", params="", query="", fragment="") + # Instantiating the ExternalIdpManager object deploys the external identity provider. + external_idp_manager = ExternalIdpManager(ops_test=ops_test) + + # Deploy the identity bundle first because it checks everything is in an active state and if we deploy JIMM apps + # at the same time, then that check will fail. + logger.info("deploying identity bundle") + await deploy_identity_bundle(ops_test=ops_test, external_idp_manager=external_idp_manager) + + # Deploy the charm and wait for active/idle status + logger.info("deploying charms") + async with ops_test.fast_forward(): + await asyncio.gather( + ops_test.model.deploy( + charm, + resources=resources, + application_name=APP_NAME, + series="focal", + config={ + "uuid": "f4dec11e-e2b6-40bb-871a-cc38e958af49", + "dns-name": jimm_address.netloc, + "final-redirect-url": os.path.join(jimm_address.geturl(), "debug/info"), + "public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", + "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", + "postgres-secret-storage": True, + }, + ), + ops_test.model.deploy("nginx-ingress-integrator", application_name="jimm-ingress", channel="latest/stable"), + ops_test.model.deploy( + "postgresql-k8s", + application_name="jimm-db", + channel="14/stable", + ), + ops_test.model.deploy( + "openfga-k8s", + application_name="openfga", + channel="latest/stable", + ), + ) + + logger.info("waiting for postgresql") + await ops_test.model.wait_for_idle( + apps=["jimm-db"], + status="active", + raise_on_blocked=True, + timeout=2000, + ) + + logger.info("adding custom ca cert relation") + await ops_test.model.relate("{}:receive-ca-cert".format(APP_NAME), APPS.SELF_SIGNED_CERTIFICATES) + + logger.info("adding ingress relation") + await ops_test.model.relate("{}:nginx-route".format(APP_NAME), "jimm-ingress") + + logger.info("adding openfga postgresql relation") + await ops_test.model.relate("openfga:database", "jimm-db:database") + + logger.info("adding openfga relation") + await ops_test.model.relate(APP_NAME, "openfga") + + logger.info("adding postgresql relation") + await ops_test.model.relate(APP_NAME, "jimm-db:database") + + logger.info("adding ouath relation") + await ops_test.model.integrate(f"{APP_NAME}:oauth", APPS.HYDRA) + + logger.info("waiting for jimm to be blocked pending auth model creation") + await ops_test.model.wait_for_idle( + apps=[APP_NAME], + status="blocked", + timeout=2000, + ) + + logger.info("running the create authorization model action") + jimm_unit = await utils.get_unit_by_name(APP_NAME, "0", ops_test.model.units) + with open("../../local/openfga/authorisation_model.json", "r") as model_file: + model_data = model_file.read() + for i in range(10): + action: Action = await jimm_unit.run_action( + "create-authorization-model", + model=model_data, + ) + result = await action.wait() + logger.info("attempt {} -> action result {} {}".format(i, result.status, result.results)) + if result.results.get("return-code") == 0: + break + time.sleep(2) + + await ops_test.model.wait_for_idle(timeout=2000) + jimm_debug_info = requests.get(os.path.join(jimm_address.geturl(), "debug/info")) + assert jimm_debug_info.status_code == 200 + logger.info("jimm info = %s", jimm_debug_info.json()) + return JimmEnv(jimm_address, external_idp_manager) diff --git a/charms/jimm-k8s/tests/unit/test_charm.py b/charms/jimm-k8s/tests/unit/test_charm.py index bb67eafcc..1ce23e112 100644 --- a/charms/jimm-k8s/tests/unit/test_charm.py +++ b/charms/jimm-k8s/tests/unit/test_charm.py @@ -21,7 +21,7 @@ "introspection_endpoint": "https://example.oidc.com/admin/oauth2/introspect", "issuer_url": "https://example.oidc.com", "jwks_endpoint": "https://example.oidc.com/.well-known/jwks.json", - "scope": "openid profile email phone", + "scope": "openid profile email offline_access", "token_endpoint": "https://example.oidc.com/oauth2/token", "userinfo_endpoint": "https://example.oidc.com/userinfo", } @@ -58,9 +58,9 @@ "JIMM_OAUTH_CLIENT_ID": OAUTH_CLIENT_ID, "JIMM_OAUTH_CLIENT_SECRET": OAUTH_CLIENT_SECRET, "JIMM_OAUTH_SCOPES": OAUTH_PROVIDER_INFO["scope"], - "JIMM_DASHBOARD_FINAL_REDIRECT_URL:": "some-url", - "JIMM_SECURE_SESSION_COOKIES:": True, - "JIMM_SESSION_COOKIE_MAX_AGE:": 86400, + "JIMM_DASHBOARD_FINAL_REDIRECT_URL": "some-url", + "JIMM_SECURE_SESSION_COOKIES": True, + "JIMM_SESSION_COOKIE_MAX_AGE": 86400, } # The environment may optionally include Vault. @@ -298,7 +298,7 @@ def test_app_dns_address(self): self.harness.update_config(MINIMAL_CONFIG) self.harness.update_config({"dns-name": "jimm.com"}) oauth_client = self.harness.charm._oauth_client_config - self.assertEqual(oauth_client.redirect_uri, "https://jimm.com/oauth/callback") + self.assertEqual(oauth_client.redirect_uri, "https://jimm.com/auth/callback") def test_app_enters_block_states_if_oauth_relation_removed(self): self.harness.update_config(MINIMAL_CONFIG) diff --git a/charms/jimm-k8s/tox.ini b/charms/jimm-k8s/tox.ini index c273f9130..24ab6b49d 100644 --- a/charms/jimm-k8s/tox.ini +++ b/charms/jimm-k8s/tox.ini @@ -61,9 +61,16 @@ commands = [testenv:integration] description = Run integration tests deps = + pytest-asyncio~=0.21 + aiohttp + asyncstdlib juju~=3.2 pytest pytest-operator + lightkube + pytest-playwright + git+https://github.com/canonical/iam-bundle@671a33419869fab1fe63d81873b41f6b181498e3#egg=oauth_tools -r{toxinidir}/requirements.txt commands = + playwright install pytest {[vars]tst_path}integration -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} diff --git a/cmd/jimmsrv/main.go b/cmd/jimmsrv/main.go index da83d011d..357d3ba57 100644 --- a/cmd/jimmsrv/main.go +++ b/cmd/jimmsrv/main.go @@ -110,6 +110,7 @@ func start(ctx context.Context, s *service.Service) error { for i, scope := range scopesParsed { scopesParsed[i] = strings.TrimSpace(scope) } + zapctx.Info(ctx, "oauth scopes", zap.Any("scopes", scopesParsed)) if len(scopesParsed) == 0 { zapctx.Error(ctx, "no oauth client scopes present") return errors.E("no oauth client scopes present") diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index bc448dd24..a56dd73f8 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -150,14 +150,14 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP // AuthCodeURL returns a URL that will be used to redirect a browser to the identity provider. func (as *AuthenticationService) AuthCodeURL() string { - // As we're not the browser creating the auth code url and then communicating back - // to the server, it is OK not to set a state as there's no communication - // between say many "tabs" and a JIMM deployment, but rather - // just JIMM creating the auth code URL itself, and then handling the exchanging - // itself. Of course, middleman attacks between the IdP and JIMM are possible, - // but we'd have much larger problems than an auth code interception at that - // point. As such, we're opting out of using auth code URL state. - return as.oauthConfig.AuthCodeURL("") + // Hydra requires the state parameter to be at least 8 characters. + // Note that state is primarily a guard against csrf attacks. + // A good reference is https://spring.io/blog/2011/11/30/cross-site-request-forgery-and-oauth2 + // Because Hydra only accepts return addresses that have been pre-registered + // the risk of csrf attacks is largely eliminated, but this may not be the case with other IdPs. + + // Note that Hydra requires a state parameter of at least 8 characters. + return as.oauthConfig.AuthCodeURL("12345678") } // Exchange exchanges an authorisation code for an access token. diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 9c8b522b9..c106843f3 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -67,7 +67,7 @@ func TestAuthCodeURL(t *testing.T) { c.Assert( url, qt.Equals, - `http://localhost:8082/realms/jimm/protocol/openid-connect/auth?client_id=jimm-device&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth%2Fcallback&response_type=code&scope=openid+profile+email`, + `http://localhost:8082/realms/jimm/protocol/openid-connect/auth?client_id=jimm-device&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth%2Fcallback&response_type=code&scope=openid+profile+email&state=12345678`, ) } diff --git a/service.go b/service.go index 414009449..e0e7be210 100644 --- a/service.go +++ b/service.go @@ -367,6 +367,7 @@ func NewService(ctx context.Context, p Params) (*Service, error) { SecureCookies: p.SecureSessionCookies, }) if err != nil { + zapctx.Error(ctx, "failed to setup authentication handler", zap.Error(err)) return nil, errors.E(op, err, "failed to setup authentication handler") } mountHandler( From 22463865fb44fb264ee7f91289b512b946c6f242 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Wed, 8 May 2024 12:35:50 +0200 Subject: [PATCH 119/126] CSS-8386 Use oauth state (#1208) * Enforce use of state parameter in browser flow * Tweak status codes * Improvements to error messages and state encoding --- internal/auth/oauth2.go | 21 +++++-- internal/auth/oauth2_test.go | 8 ++- internal/jimmhttp/auth_handler.go | 68 ++++++++++++++++----- internal/jimmhttp/auth_handler_test.go | 84 +++++++++++++++++++++----- internal/jimmtest/auth.go | 13 ++-- local/traefik/certs/certs.sh | 1 + service.go | 5 +- 7 files changed, 155 insertions(+), 45 deletions(-) diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index a56dd73f8..5f0cbb066 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -10,6 +10,7 @@ package auth import ( "context" + "crypto/rand" "encoding/base64" stderrors "errors" "fmt" @@ -39,6 +40,9 @@ const ( // SessionIdentityKey is the key for the identity value stored within the // session. SessionIdentityKey = "identity-id" + + // StateKey is the key for the OAuth callback state stored within a user's cookie. + StateKey = "jimm-oauth-state" ) type sessionIdentityContextKey struct{} @@ -149,15 +153,22 @@ func NewAuthenticationService(ctx context.Context, params AuthenticationServiceP } // AuthCodeURL returns a URL that will be used to redirect a browser to the identity provider. -func (as *AuthenticationService) AuthCodeURL() string { +// It also generates a random state string that was used as part of the auth code URL. The state string +// is returned alongside the auth code URL and any errors that occured during state generation. +func (as *AuthenticationService) AuthCodeURL() (string, string, error) { // Hydra requires the state parameter to be at least 8 characters. // Note that state is primarily a guard against csrf attacks. // A good reference is https://spring.io/blog/2011/11/30/cross-site-request-forgery-and-oauth2 // Because Hydra only accepts return addresses that have been pre-registered // the risk of csrf attacks is largely eliminated, but this may not be the case with other IdPs. - - // Note that Hydra requires a state parameter of at least 8 characters. - return as.oauthConfig.AuthCodeURL("12345678") + const op = errors.Op("AuthenticationService.AuthCodeURL") + b := make([]byte, 8) + _, err := rand.Read(b) + if err != nil { + return "", "", errors.E(op, fmt.Sprintf("failed to generate state secret: %s", err.Error())) + } + state := base64.RawURLEncoding.EncodeToString(b) + return as.oauthConfig.AuthCodeURL(state), state, nil } // Exchange exchanges an authorisation code for an access token. @@ -394,7 +405,7 @@ func (as *AuthenticationService) AuthenticateBrowserSession(ctx context.Context, identityId, ok := session.Values[SessionIdentityKey] if !ok { - return ctx, errors.E(op, "session is missing identity key") + return ctx, errors.E(op, errors.CodeForbidden, "session is missing identity key") } err = as.validateAndUpdateAccessToken(ctx, identityId) diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index c106843f3..552ea9470 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -63,12 +63,14 @@ func TestAuthCodeURL(t *testing.T) { authSvc, _, _ := setupTestAuthSvc(ctx, c, time.Hour) - url := authSvc.AuthCodeURL() + url, state, err := authSvc.AuthCodeURL() + c.Assert(err, qt.IsNil) c.Assert( url, - qt.Equals, - `http://localhost:8082/realms/jimm/protocol/openid-connect/auth?client_id=jimm-device&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth%2Fcallback&response_type=code&scope=openid+profile+email&state=12345678`, + qt.Matches, + regexp.MustCompile(`http:\/\/localhost:8082\/realms\/jimm\/protocol\/openid-connect\/auth\?client_id=jimm-device&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth%2Fcallback&response_type=code&scope=openid\+profile\+email&state=.*`), ) + c.Assert(len(state), qt.Not(qt.Equals), 0) } // TestDevice is a unique test in that it runs through the entire device oauth2.0 diff --git a/internal/jimmhttp/auth_handler.go b/internal/jimmhttp/auth_handler.go index cfaed76d0..8fe156136 100644 --- a/internal/jimmhttp/auth_handler.go +++ b/internal/jimmhttp/auth_handler.go @@ -15,10 +15,15 @@ import ( "github.com/canonical/jimm/internal/errors" ) -// CallbackEndpoint holds the endpoint path for OAuth2.0 authorisation -// flow callbacks. +// These consts holds the endpoint paths for OAuth2.0 related auth. +// AuthResourceBasePath forms the base path and the remainder are +// appended onto the base in practice. const ( - CallbackEndpoint = "/callback" + AuthResourceBasePath = "/auth" + CallbackEndpoint = "/callback" + WhoAmIEndpoint = "/whoami" + LogOutEndpoint = "/logout" + LoginEndpoint = "/login" ) // OAuthHandler handles the oauth2.0 browser flow for JIMM. @@ -47,7 +52,7 @@ type OAuthHandlerParams struct { // BrowserOAuthAuthenticator handles authorisation code authentication within JIMM // via OIDC. type BrowserOAuthAuthenticator interface { - AuthCodeURL() string + AuthCodeURL() (string, string, error) Exchange(ctx context.Context, code string) (*oauth2.Token, error) UserInfo(ctx context.Context, oauth2Token *oauth2.Token) (string, error) UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error @@ -82,10 +87,10 @@ func NewOAuthHandler(p OAuthHandlerParams) (*OAuthHandler, error) { // Routes returns the grouped routers routes with group specific middlewares. func (oah *OAuthHandler) Routes() chi.Router { oah.SetupMiddleware() - oah.Router.Get("/login", oah.Login) + oah.Router.Get(LoginEndpoint, oah.Login) oah.Router.Get(CallbackEndpoint, oah.Callback) - oah.Router.Get("/logout", oah.Logout) - oah.Router.Get("/whoami", oah.Whoami) + oah.Router.Get(LogOutEndpoint, oah.Logout) + oah.Router.Get(WhoAmIEndpoint, oah.Whoami) return oah.Router } @@ -95,7 +100,20 @@ func (oah *OAuthHandler) SetupMiddleware() { // Login handles /auth/login. func (oah *OAuthHandler) Login(w http.ResponseWriter, r *http.Request) { - redirectURL := oah.authenticator.AuthCodeURL() + ctx := r.Context() + redirectURL, state, err := oah.authenticator.AuthCodeURL() + if err != nil { + writeError(ctx, w, http.StatusInternalServerError, err, "failed to generate auth redirect URL") + return + } + http.SetCookie(w, &http.Cookie{ + Name: auth.StateKey, + Value: state, + MaxAge: 900, // 15 min. + Path: AuthResourceBasePath + CallbackEndpoint, // Only send the cookie back on /auth paths. + HttpOnly: true, // Restrict access from JS. + SameSite: http.SameSiteStrictMode, // Cannot be sent cross-origin. + }) http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect) } @@ -103,9 +121,23 @@ func (oah *OAuthHandler) Login(w http.ResponseWriter, r *http.Request) { func (oah *OAuthHandler) Callback(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + stateByCookie, err := r.Cookie(auth.StateKey) + if err != nil { + usrErr := errors.E("no state cookie present") + writeError(ctx, w, http.StatusForbidden, usrErr, "no state cookie present") + return + } + stateByURL := r.URL.Query().Get("state") + if stateByCookie.Value != stateByURL { + err := errors.E("state does not match") + writeError(ctx, w, http.StatusForbidden, err, "state does not match") + return + } + code := r.URL.Query().Get("code") if code == "" { - writeError(ctx, w, http.StatusBadRequest, nil, "no authorisation code present") + err := errors.E("missing auth code") + writeError(ctx, w, http.StatusForbidden, err, "no authorisation code present") return } @@ -113,18 +145,18 @@ func (oah *OAuthHandler) Callback(w http.ResponseWriter, r *http.Request) { token, err := authSvc.Exchange(ctx, code) if err != nil { - writeError(ctx, w, http.StatusBadRequest, err, "failed to exchange authcode") + writeError(ctx, w, http.StatusForbidden, err, "failed to exchange authcode") return } email, err := authSvc.UserInfo(ctx, token) if err != nil { - writeError(ctx, w, http.StatusBadRequest, err, "failed to retrieve user info") + writeError(ctx, w, http.StatusInternalServerError, err, "failed to retrieve user info") return } if err := authSvc.UpdateIdentity(ctx, email, token); err != nil { - writeError(ctx, w, http.StatusBadRequest, err, "failed to update identity") + writeError(ctx, w, http.StatusInternalServerError, err, "failed to update identity") return } @@ -135,7 +167,7 @@ func (oah *OAuthHandler) Callback(w http.ResponseWriter, r *http.Request) { oah.secureCookies, email, ); err != nil { - writeError(ctx, w, http.StatusBadRequest, err, "failed to setup session") + writeError(ctx, w, http.StatusInternalServerError, err, "failed to setup session") } http.Redirect(w, r, oah.dashboardFinalRedirectURL, http.StatusPermanentRedirect) @@ -172,6 +204,10 @@ func (oah *OAuthHandler) Whoami(w http.ResponseWriter, r *http.Request) { ctx, err := authSvc.AuthenticateBrowserSession(ctx, w, r) if err != nil { + if errors.ErrorCode(err) == errors.CodeForbidden { + w.WriteHeader(http.StatusForbidden) + return + } writeError(ctx, w, http.StatusInternalServerError, err, "failed to authenticate users session") return } @@ -201,5 +237,9 @@ func (oah *OAuthHandler) Whoami(w http.ResponseWriter, r *http.Request) { func writeError(ctx context.Context, w http.ResponseWriter, status int, err error, logMessage string) { zapctx.Error(ctx, logMessage, zap.Error(err)) w.WriteHeader(status) - w.Write([]byte(http.StatusText(status))) + errMsg := "" + if err != nil { + errMsg = " - " + err.Error() + } + w.Write([]byte(http.StatusText(status) + errMsg)) } diff --git a/internal/jimmhttp/auth_handler_test.go b/internal/jimmhttp/auth_handler_test.go index e91adf84b..f0e4aca22 100644 --- a/internal/jimmhttp/auth_handler_test.go +++ b/internal/jimmhttp/auth_handler_test.go @@ -4,6 +4,9 @@ import ( "context" "io" "net/http" + "net/http/cookiejar" + "net/http/httptest" + "net/url" "testing" "time" @@ -12,7 +15,9 @@ import ( "github.com/gorilla/sessions" "github.com/canonical/jimm/api/params" + "github.com/canonical/jimm/internal/auth" "github.com/canonical/jimm/internal/db" + "github.com/canonical/jimm/internal/jimmhttp" "github.com/canonical/jimm/internal/jimmtest" ) @@ -32,6 +37,16 @@ func setupDbAndSessionStore(c *qt.C) (*db.Database, sessions.Store) { return db, store } +func createClientWithStateCookie(c *qt.C, s *httptest.Server) *http.Client { + jar, err := cookiejar.New(nil) + c.Assert(err, qt.IsNil) + jimmURL, err := url.Parse(s.URL) + c.Assert(err, qt.IsNil) + stateCookie := http.Cookie{Name: auth.StateKey, Value: "123"} + jar.SetCookies(jimmURL, []*http.Cookie{&stateCookie}) + return &http.Client{Jar: jar} +} + // TestBrowserLoginAndLogout goes through the flow of a browser logging in, simulating // the cookie state and handling the callbacks are as expected. Additionally handling // the final callback to the dashboard emulating an endpoint. See RunBrowserLogin @@ -59,7 +74,7 @@ func TestBrowserLoginAndLogout(t *testing.T) { c.Assert(cookie, qt.Not(qt.Equals), "") // Run a whoami logged in - req, err := http.NewRequest("GET", jimmHTTPServer.URL+"/whoami", nil) + req, err := http.NewRequest("GET", jimmHTTPServer.URL+jimmhttp.AuthResourceBasePath+jimmhttp.WhoAmIEndpoint, nil) c.Assert(err, qt.IsNil) parsedCookies := jimmtest.ParseCookies(cookie) c.Assert(parsedCookies, qt.HasLen, 1) @@ -77,7 +92,7 @@ func TestBrowserLoginAndLogout(t *testing.T) { }) // Logout - req, err = http.NewRequest("GET", jimmHTTPServer.URL+"/logout", nil) + req, err = http.NewRequest("GET", jimmHTTPServer.URL+jimmhttp.AuthResourceBasePath+jimmhttp.LogOutEndpoint, nil) c.Assert(err, qt.IsNil) req.AddCookie(parsedCookies[0]) @@ -87,7 +102,7 @@ func TestBrowserLoginAndLogout(t *testing.T) { c.Assert(res.StatusCode, qt.Equals, http.StatusOK) // Run a whoami logged out - req, err = http.NewRequest("GET", jimmHTTPServer.URL+"/whoami", nil) + req, err = http.NewRequest("GET", jimmHTTPServer.URL+jimmhttp.AuthResourceBasePath+jimmhttp.WhoAmIEndpoint, nil) c.Assert(err, qt.IsNil) parsedCookies = jimmtest.ParseCookies(cookie) c.Assert(parsedCookies, qt.HasLen, 1) @@ -96,15 +111,10 @@ func TestBrowserLoginAndLogout(t *testing.T) { res, err = http.DefaultClient.Do(req) c.Assert(err, qt.IsNil) defer res.Body.Close() - c.Assert(res.StatusCode, qt.Equals, http.StatusInternalServerError) - b, err = io.ReadAll(res.Body) - c.Assert(err, qt.IsNil) - // TODO(ale8k): Really it isn't an internal server error here, the session is just - // missing in our store, we should probably bring this error up and return a forbidden. - c.Assert(string(b), qt.Equals, "Internal Server Error") + c.Assert(res.StatusCode, qt.Equals, http.StatusForbidden) // Run a logout with no identity - req, err = http.NewRequest("GET", jimmHTTPServer.URL+"/logout", nil) + req, err = http.NewRequest("GET", jimmHTTPServer.URL+jimmhttp.AuthResourceBasePath+jimmhttp.LogOutEndpoint, nil) c.Assert(err, qt.IsNil) res, err = http.DefaultClient.Do(req) c.Assert(err, qt.IsNil) @@ -112,6 +122,44 @@ func TestBrowserLoginAndLogout(t *testing.T) { c.Assert(res.StatusCode, qt.Equals, http.StatusForbidden) } +func TestCallbackFailsNoState(t *testing.T) { + c := qt.New(t) + + db, sessionStore := setupDbAndSessionStore(c) + s, err := jimmtest.SetupTestDashboardCallbackHandler("", db, sessionStore) + c.Assert(err, qt.IsNil) + defer s.Close() + + callbackURL := s.URL + jimmhttp.AuthResourceBasePath + jimmhttp.CallbackEndpoint + res, err := http.Get(callbackURL) + c.Assert(err, qt.IsNil) + + defer res.Body.Close() + + b, err := io.ReadAll(res.Body) + c.Assert(err, qt.IsNil) + c.Assert(string(b), qt.Equals, http.StatusText(http.StatusForbidden)+" - no state cookie present") +} + +func TestCallbackFailsStateNoMatch(t *testing.T) { + c := qt.New(t) + + db, sessionStore := setupDbAndSessionStore(c) + s, err := jimmtest.SetupTestDashboardCallbackHandler("", db, sessionStore) + c.Assert(err, qt.IsNil) + defer s.Close() + + client := createClientWithStateCookie(c, s) + callbackURL := s.URL + jimmhttp.AuthResourceBasePath + jimmhttp.CallbackEndpoint + res, err := client.Get(callbackURL + "?state=567") + c.Assert(err, qt.IsNil) + + defer res.Body.Close() + b, err := io.ReadAll(res.Body) + c.Assert(err, qt.IsNil) + c.Assert(string(b), qt.Equals, http.StatusText(http.StatusForbidden)+" - state does not match") +} + func TestCallbackFailsNoCodePresent(t *testing.T) { c := qt.New(t) @@ -120,15 +168,17 @@ func TestCallbackFailsNoCodePresent(t *testing.T) { c.Assert(err, qt.IsNil) defer s.Close() - // Test with no code present at all - res, err := http.Get(s.URL + "/callback") + client := createClientWithStateCookie(c, s) + + callbackURL := s.URL + jimmhttp.AuthResourceBasePath + jimmhttp.CallbackEndpoint + res, err := client.Get(callbackURL + "?state=123") c.Assert(err, qt.IsNil) defer res.Body.Close() b, err := io.ReadAll(res.Body) c.Assert(err, qt.IsNil) - c.Assert(string(b), qt.Equals, http.StatusText(http.StatusBadRequest)) + c.Assert(string(b), qt.Equals, http.StatusText(http.StatusForbidden)+" - missing auth code") } func TestCallbackFailsExchange(t *testing.T) { @@ -139,13 +189,15 @@ func TestCallbackFailsExchange(t *testing.T) { c.Assert(err, qt.IsNil) defer s.Close() - // Test with no code present at all - res, err := http.Get(s.URL + "/callback?code=idonotexist") + client := createClientWithStateCookie(c, s) + callbackURL := s.URL + jimmhttp.AuthResourceBasePath + jimmhttp.CallbackEndpoint + c.Assert(err, qt.IsNil) + res, err := client.Get(callbackURL + "?code=idonotexist&state=123") c.Assert(err, qt.IsNil) defer res.Body.Close() b, err := io.ReadAll(res.Body) c.Assert(err, qt.IsNil) - c.Assert(string(b), qt.Equals, http.StatusText(http.StatusBadRequest)) + c.Assert(string(b), qt.Equals, http.StatusText(http.StatusForbidden)+" - authorisation code exchange failed") } diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index 31097a4fc..4643f7336 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -19,6 +19,7 @@ import ( "time" "github.com/coreos/go-oidc/v3/oidc" + "github.com/go-chi/chi/v5" "github.com/gorilla/sessions" "github.com/juju/juju/api" jujuparams "github.com/juju/juju/rpc/params" @@ -118,7 +119,7 @@ func SetupTestDashboardCallbackHandler(browserURL string, db *db.Database, sessi s.Listener = listener // Remember redirect url to check it matches after test server starts - redirectURL := "http://127.0.0.1:" + port + "/callback" + redirectURL := "http://127.0.0.1:" + port + jimmhttp.AuthResourceBasePath + jimmhttp.CallbackEndpoint authSvc, err := auth.NewAuthenticationService(context.Background(), auth.AuthenticationServiceParams{ IssuerURL: "http://localhost:8082/realms/jimm", ClientID: "jimm-device", @@ -144,12 +145,15 @@ func SetupTestDashboardCallbackHandler(browserURL string, db *db.Database, sessi return nil, err } - s.Config.Handler = h.Routes() + mux := chi.NewMux() + mux.Mount(jimmhttp.AuthResourceBasePath, h.Routes()) + s.Config.Handler = mux s.Start() // Ensure redirectURL is matching port on listener - if s.URL+"/callback" != redirectURL { + callbackURL := s.URL + jimmhttp.AuthResourceBasePath + jimmhttp.CallbackEndpoint + if callbackURL != redirectURL { return s, errors.New("server callback does not match redirectURL") } @@ -208,7 +212,8 @@ func runBrowserLogin(db *db.Database, sessionStore sessions.Store, username, pas }, } - res, err := client.Get(s.URL + "/login") + loginURL := s.URL + jimmhttp.AuthResourceBasePath + jimmhttp.LoginEndpoint + res, err := client.Get(loginURL) if err != nil { return cookieString, s, err } diff --git a/local/traefik/certs/certs.sh b/local/traefik/certs/certs.sh index b9d004c1b..bbe76c556 100755 --- a/local/traefik/certs/certs.sh +++ b/local/traefik/certs/certs.sh @@ -7,6 +7,7 @@ if [ "$1" != "--force" ]; then if [ -f "server.crt" ] && [ -f "server.key" ]; then echo "Server certs already exist. Skipping cert generation." echo "Run with --force to regenerate." + exit 0 fi fi diff --git a/service.go b/service.go index e0e7be210..c0d8eec72 100644 --- a/service.go +++ b/service.go @@ -292,8 +292,7 @@ func NewService(ctx context.Context, p Params) (*Service, error) { return nil, errors.E(op, err, "failed to ensure controller admins") } - authResourceBasePath := "/auth" - redirectUrl := p.PublicDNSName + authResourceBasePath + jimmhttp.CallbackEndpoint + redirectUrl := p.PublicDNSName + jimmhttp.AuthResourceBasePath + jimmhttp.CallbackEndpoint if !strings.HasPrefix(redirectUrl, "https://") || !strings.HasPrefix(redirectUrl, "http://") { redirectUrl = "https://" + redirectUrl } @@ -371,7 +370,7 @@ func NewService(ctx context.Context, p Params) (*Service, error) { return nil, errors.E(op, err, "failed to setup authentication handler") } mountHandler( - authResourceBasePath, + jimmhttp.AuthResourceBasePath, oauthHandler, ) macaroonDischarger, err := s.setupDischarger(p) From b28071501b7beb9e01bcf00723095cfa788333cc Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Thu, 9 May 2024 10:17:01 +0200 Subject: [PATCH 120/126] Change cookie to lax (#1209) Allow cookie to be sent cross-origin for OAuth browser flow login. --- docker-compose.yaml | 2 +- internal/jimmhttp/auth_handler.go | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index d14898671..d9fa3bdda 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -71,7 +71,7 @@ services: JIMM_OAUTH_CLIENT_ID: "jimm-device" JIMM_OAUTH_CLIENT_SECRET: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4" JIMM_OAUTH_SCOPES: "openid profile email" # Space separated list of scopes - JIMM_DASHBOARD_FINAL_REDIRECT_URL: "https://my-dashboard.com/final-callback" # Example URL + JIMM_DASHBOARD_FINAL_REDIRECT_URL: "https://jaas.ai" # Example URL JIMM_ACCESS_TOKEN_EXPIRY_DURATION: 1h JIMM_SECURE_SESSION_COOKIES: false JIMM_SESSION_COOKIE_MAX_AGE: 86400 diff --git a/internal/jimmhttp/auth_handler.go b/internal/jimmhttp/auth_handler.go index a67649c76..42858b22f 100644 --- a/internal/jimmhttp/auth_handler.go +++ b/internal/jimmhttp/auth_handler.go @@ -114,7 +114,7 @@ func (oah *OAuthHandler) Login(w http.ResponseWriter, r *http.Request) { MaxAge: 900, // 15 min. Path: AuthResourceBasePath + CallbackEndpoint, // Only send the cookie back on /auth paths. HttpOnly: true, // Restrict access from JS. - SameSite: http.SameSiteStrictMode, // Cannot be sent cross-origin. + SameSite: http.SameSiteLaxMode, // Allow the cookie to be sent on a redirect from the IdP to JIMM. }) http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect) } @@ -232,12 +232,10 @@ func (oah *OAuthHandler) Whoami(w http.ResponseWriter, r *http.Request) { return } + w.Header().Add("Content-Type", "application/json") if _, err := w.Write(b); err != nil { - writeError(ctx, w, http.StatusInternalServerError, err, "failed to write response to whoami") - return + zapctx.Error(ctx, "failed to write whoami body", zap.Error(err)) } - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) } // writeError writes an error and logs the message. It is expected that the status code From 20c0552bc4703fc81373ae25143fe9fa6adcf4b0 Mon Sep 17 00:00:00 2001 From: Ales Stimec Date: Fri, 10 May 2024 09:04:10 +0200 Subject: [PATCH 121/126] Added device code grant type to the VM charm. --- charms/jimm/lib/charms/hydra/v0/oauth.py | 43 ++++++++++++------------ charms/jimm/requirements.txt | 2 +- charms/jimm/src/charm.py | 2 +- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/charms/jimm/lib/charms/hydra/v0/oauth.py b/charms/jimm/lib/charms/hydra/v0/oauth.py index 6d8ed1ef9..9b7781bf8 100644 --- a/charms/jimm/lib/charms/hydra/v0/oauth.py +++ b/charms/jimm/lib/charms/hydra/v0/oauth.py @@ -48,21 +48,14 @@ def _set_client_config(self): ``` """ -import inspect import json import logging import re -from dataclasses import asdict, dataclass, field +from dataclasses import asdict, dataclass, field, fields from typing import Dict, List, Mapping, Optional import jsonschema -from ops.charm import ( - CharmBase, - RelationBrokenEvent, - RelationChangedEvent, - RelationCreatedEvent, - RelationDepartedEvent, -) +from ops.charm import CharmBase, RelationBrokenEvent, RelationChangedEvent, RelationCreatedEvent from ops.framework import EventBase, EventSource, Handle, Object, ObjectEvents from ops.model import Relation, Secret, TooManyRelatedAppsError @@ -74,12 +67,20 @@ def _set_client_config(self): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 5 +LIBPATCH = 8 + +PYDEPS = ["jsonschema"] + logger = logging.getLogger(__name__) DEFAULT_RELATION_NAME = "oauth" -ALLOWED_GRANT_TYPES = ["authorization_code", "refresh_token", "client_credentials"] +ALLOWED_GRANT_TYPES = [ + "authorization_code", + "refresh_token", + "client_credentials", + "urn:ietf:params:oauth:grant-type:device_code", +] ALLOWED_CLIENT_AUTHN_METHODS = ["client_secret_basic", "client_secret_post"] CLIENT_SECRET_FIELD = "secret" @@ -153,13 +154,13 @@ def _set_client_config(self): "type": "array", "default": None, "items": { - "enum": ["authorization_code", "client_credentials", "refresh_token"], + "enum": ALLOWED_GRANT_TYPES, "type": "string", }, }, "token_endpoint_auth_method": { "type": "string", - "enum": ["client_secret_basic", "client_secret_post"], + "enum": ALLOWED_CLIENT_AUTHN_METHODS, "default": "client_secret_basic", }, }, @@ -295,7 +296,7 @@ class OauthProviderConfig: @classmethod def from_dict(cls, dic: Dict) -> "OauthProviderConfig": """Generate OauthProviderConfig instance from dict.""" - return cls(**{k: v for k, v in dic.items() if k in inspect.signature(cls).parameters}) + return cls(**{k: v for k, v in dic.items() if k in [f.name for f in fields(cls)]}) class OAuthInfoChangedEvent(EventBase): @@ -315,6 +316,7 @@ def snapshot(self) -> Dict: def restore(self, snapshot: Dict) -> None: """Restore event.""" + super().restore(snapshot) self.client_id = snapshot["client_id"] self.client_secret_id = snapshot["client_secret_id"] @@ -395,9 +397,6 @@ def _on_relation_broken_event(self, event: RelationBrokenEvent) -> None: self.on.oauth_info_removed.emit() def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - if not self.model.unit.is_leader(): - return - data = event.relation.data[event.app] if not data: logger.info("No relation data available.") @@ -457,7 +456,9 @@ def is_client_created(self, relation_id: Optional[int] = None) -> bool: and "client_secret_id" in relation.data[relation.app] ) - def get_provider_info(self, relation_id: Optional[int] = None) -> OauthProviderConfig: + def get_provider_info( + self, relation_id: Optional[int] = None + ) -> Optional[OauthProviderConfig]: """Get the provider information from the databag.""" if len(self.model.relations) == 0: return None @@ -650,8 +651,8 @@ def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME) self._get_client_config_from_relation_data, ) self.framework.observe( - events.relation_departed, - self._on_relation_departed, + events.relation_broken, + self._on_relation_broken, ) def _get_client_config_from_relation_data(self, event: RelationChangedEvent) -> None: @@ -699,7 +700,7 @@ def _get_client_config_from_relation_data(self, event: RelationChangedEvent) -> def _get_secret_label(self, relation: Relation) -> str: return f"client_secret_{relation.id}" - def _on_relation_departed(self, event: RelationDepartedEvent) -> None: + def _on_relation_broken(self, event: RelationBrokenEvent) -> None: # Workaround for https://github.com/canonical/operator/issues/888 self._pop_relation_data(event.relation.id) diff --git a/charms/jimm/requirements.txt b/charms/jimm/requirements.txt index d9c19d8ea..e419a9c97 100644 --- a/charms/jimm/requirements.txt +++ b/charms/jimm/requirements.txt @@ -1,6 +1,6 @@ markupsafe>=2.0.1 Jinja2 >= 2.11.3 -ops >= 2.0.0 +ops >= 2.12.0 charmhelpers >= 0.20.22 hvac >= 0.11.0 pydantic == 1.10.* diff --git a/charms/jimm/src/charm.py b/charms/jimm/src/charm.py index 0e0e39edb..ea56d7ac4 100755 --- a/charms/jimm/src/charm.py +++ b/charms/jimm/src/charm.py @@ -41,7 +41,7 @@ OAUTH = "oauth" OAUTH_SCOPES = "openid email offline_access" # TODO: Add "device_code" below once the charm interface supports it. -OAUTH_GRANT_TYPES = ["authorization_code", "refresh_token"] +OAUTH_GRANT_TYPES = ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"] # Env file parts DB_PART = "db" From a7e44b5f44856f0c55c174404f9081b57fe14f44 Mon Sep 17 00:00:00 2001 From: Ales Stimec Date: Tue, 14 May 2024 09:39:20 +0200 Subject: [PATCH 122/126] Adds UUID to groups And uses uuid to refer to groups in OpenFGA instead of ID, which is assigned by DB. --- api/params/params.go | 1 + cmd/jimmctl/cmd/group_test.go | 1 + internal/db/export_test.go | 1 + internal/db/group.go | 16 ++++++++++ internal/db/group_test.go | 19 ++++++++++++ internal/dbmodel/group.go | 7 +++-- internal/dbmodel/sql/postgres/1_7.sql | 4 +++ internal/dbmodel/version.go | 2 +- internal/jimm/access.go | 14 ++------- internal/jimm/access_test.go | 2 +- internal/jujuapi/access_control_test.go | 38 +++++++++++------------ internal/openfga/names/names_test.go | 4 +-- internal/openfga/openfga_test.go | 40 ++++++++++++++----------- internal/openfga/user_test.go | 34 ++++++++++----------- pkg/names/group.go | 2 +- pkg/names/group_test.go | 23 +++++++++----- 16 files changed, 128 insertions(+), 80 deletions(-) create mode 100644 internal/dbmodel/sql/postgres/1_7.sql diff --git a/api/params/params.go b/api/params/params.go index 00827cac3..2666833e5 100644 --- a/api/params/params.go +++ b/api/params/params.go @@ -294,6 +294,7 @@ type RemoveGroupRequest struct { // Group holds the details of a group currently residing in JIMM. type Group struct { + UUID string `json:"uuid"` Name string `json:"name"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` diff --git a/cmd/jimmctl/cmd/group_test.go b/cmd/jimmctl/cmd/group_test.go index 235f0cb2c..55a4a8162 100644 --- a/cmd/jimmctl/cmd/group_test.go +++ b/cmd/jimmctl/cmd/group_test.go @@ -33,6 +33,7 @@ func (s *groupSuite) TestAddGroupSuperuser(c *gc.C) { c.Assert(err, gc.IsNil) c.Assert(group.ID, gc.Equals, uint(1)) c.Assert(group.Name, gc.Equals, "test-group") + c.Assert(group.UUID, gc.Not(gc.Equals), "") } func (s *groupSuite) TestAddGroup(c *gc.C) { diff --git a/internal/db/export_test.go b/internal/db/export_test.go index be98a0c08..c04ace351 100644 --- a/internal/db/export_test.go +++ b/internal/db/export_test.go @@ -9,4 +9,5 @@ var ( JwksExpiryTag = jwksExpiryTag OAuthKind = oauthKind OAuthKeyTag = oauthKeyTag + NewUUID = &newUUID ) diff --git a/internal/db/group.go b/internal/db/group.go index 816a6a4be..8f3b794c4 100644 --- a/internal/db/group.go +++ b/internal/db/group.go @@ -5,10 +5,16 @@ package db import ( "context" + "github.com/google/uuid" + "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" ) +var newUUID = func() string { + return uuid.NewString() +} + // AddGroup adds a new group. func (d *Database) AddGroup(ctx context.Context, name string) error { const op = errors.Op("db.AddGroup") @@ -17,6 +23,7 @@ func (d *Database) AddGroup(ctx context.Context, name string) error { } ge := dbmodel.GroupEntry{ Name: name, + UUID: newUUID(), } if err := d.DB.WithContext(ctx).Create(&ge).Error; err != nil { @@ -36,6 +43,9 @@ func (d *Database) GetGroup(ctx context.Context, group *dbmodel.GroupEntry) erro if group.ID != 0 { db = db.Where("id = ?", group.ID) } + if group.UUID != "" { + db = db.Where("uuid = ?", group.UUID) + } if group.Name != "" { db = db.Where("name = ?", group.Name) } @@ -83,6 +93,9 @@ func (d *Database) UpdateGroup(ctx context.Context, group *dbmodel.GroupEntry) e if group.ID == 0 { return errors.E(errors.CodeNotFound) } + if group.UUID == "" { + return errors.E("group uuid not specified", errors.CodeNotFound) + } if err := d.DB.WithContext(ctx).Save(group).Error; err != nil { return errors.E(op, dbError(err)) } @@ -98,6 +111,9 @@ func (d *Database) RemoveGroup(ctx context.Context, group *dbmodel.GroupEntry) e if group.ID == 0 { return errors.E(errors.CodeNotFound) } + if group.UUID == "" { + return errors.E(errors.CodeNotFound) + } if err := d.DB.WithContext(ctx).Delete(group).Error; err != nil { return errors.E(op, dbError(err)) } diff --git a/internal/db/group_test.go b/internal/db/group_test.go index 6d9ccb342..7d01cba01 100644 --- a/internal/db/group_test.go +++ b/internal/db/group_test.go @@ -7,6 +7,7 @@ import ( "testing" qt "github.com/frankban/quicktest" + "github.com/google/uuid" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" @@ -25,6 +26,11 @@ func TestAddGroupUnconfiguredDatabase(t *testing.T) { func (s *dbSuite) TestAddGroup(c *qt.C) { ctx := context.Background() + uuid := uuid.NewString() + c.Patch(db.NewUUID, func() string { + return uuid + }) + err := s.Database.AddGroup(ctx, "test-group") c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeUpgradeInProgress) @@ -44,9 +50,15 @@ func (s *dbSuite) TestAddGroup(c *qt.C) { c.Assert(tx.Error, qt.IsNil) c.Assert(ge.ID, qt.Equals, uint(1)) c.Assert(ge.Name, qt.Equals, "test-group") + c.Assert(ge.UUID, qt.Equals, uuid) } func (s *dbSuite) TestGetGroup(c *qt.C) { + uuid1 := uuid.NewString() + c.Patch(db.NewUUID, func() string { + return uuid1 + }) + err := s.Database.GetGroup(context.Background(), &dbmodel.GroupEntry{ Name: "test-group", }) @@ -68,6 +80,12 @@ func (s *dbSuite) TestGetGroup(c *qt.C) { c.Check(err, qt.IsNil) c.Assert(group.ID, qt.Equals, uint(1)) c.Assert(group.Name, qt.Equals, "test-group") + c.Assert(group.UUID, qt.Equals, uuid1) + + uuid2 := uuid.NewString() + c.Patch(db.NewUUID, func() string { + return uuid2 + }) err = s.Database.AddGroup(context.Background(), "test-group1") c.Assert(err, qt.IsNil) @@ -80,6 +98,7 @@ func (s *dbSuite) TestGetGroup(c *qt.C) { c.Check(err, qt.IsNil) c.Assert(group.ID, qt.Equals, uint(2)) c.Assert(group.Name, qt.Equals, "test-group1") + c.Assert(group.UUID, qt.Equals, uuid2) } func (s *dbSuite) TestUpdateGroup(c *qt.C) { diff --git a/internal/dbmodel/group.go b/internal/dbmodel/group.go index ecb41a869..f4535fb4a 100644 --- a/internal/dbmodel/group.go +++ b/internal/dbmodel/group.go @@ -3,7 +3,6 @@ package dbmodel import ( - "strconv" "time" "github.com/juju/names/v5" @@ -19,12 +18,16 @@ type GroupEntry struct { // Name holds the name of the group. Name string `gorm:"index;column:name"` + + // UUID holds the uuid of the group. + UUID string `gotm:"index;column:uuid"` } // ToAPIGroup converts a group entry to a JIMM API // Group. func (g GroupEntry) ToAPIGroupEntry() apiparams.Group { var group apiparams.Group + group.UUID = g.UUID group.Name = g.Name group.CreatedAt = g.CreatedAt.Format(time.RFC3339) group.UpdatedAt = g.UpdatedAt.Format(time.RFC3339) @@ -47,5 +50,5 @@ func (g *GroupEntry) Tag() names.Tag { // a concrete type names.GroupTag instead of the // names.Tag interface. func (g *GroupEntry) ResourceTag() jimmnames.GroupTag { - return jimmnames.NewGroupTag(strconv.Itoa(int(g.ID))) + return jimmnames.NewGroupTag(g.UUID) } diff --git a/internal/dbmodel/sql/postgres/1_7.sql b/internal/dbmodel/sql/postgres/1_7.sql new file mode 100644 index 000000000..bc74e93b5 --- /dev/null +++ b/internal/dbmodel/sql/postgres/1_7.sql @@ -0,0 +1,4 @@ +-- 1_7.sql is a migration that adds a UUID column to the identity table. +ALTER TABLE groups ADD COLUMN uuid TEXT NOT NULL UNIQUE; + +UPDATE versions SET major=1, minor=7 WHERE component='jimmdb'; diff --git a/internal/dbmodel/version.go b/internal/dbmodel/version.go index 56730709f..2c113e675 100644 --- a/internal/dbmodel/version.go +++ b/internal/dbmodel/version.go @@ -20,7 +20,7 @@ const ( // Minor is the minor version of the model described in the dbmodel // package. It should be incremented for any change made to the // database model from database model in a released JIMM. - Minor = 6 + Minor = 7 ) type Version struct { diff --git a/internal/jimm/access.go b/internal/jimm/access.go index eb7268f38..b02c2b2a2 100644 --- a/internal/jimm/access.go +++ b/internal/jimm/access.go @@ -7,7 +7,6 @@ import ( "database/sql" "fmt" "regexp" - "strconv" "strings" "sync" @@ -18,7 +17,6 @@ import ( "github.com/juju/zaputil" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" - "gorm.io/gorm" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" @@ -442,16 +440,10 @@ func (j *JIMM) ToJAASTag(ctx context.Context, tag *ofganames.Tag) (string, error } return aoString, nil case jimmnames.GroupTagKind: - id, err := strconv.ParseUint(tag.ID, 10, 32) - if err != nil { - return "", errors.E(err, fmt.Sprintf("failed to parse group id: %v", tag.ID)) - } group := dbmodel.GroupEntry{ - Model: gorm.Model{ - ID: uint(id), - }, + UUID: tag.ID, } - err = j.Database.GetGroup(ctx, &group) + err := j.Database.GetGroup(ctx, &group) if err != nil { return "", errors.E(err, "failed to fetch group information") } @@ -532,7 +524,7 @@ func resolveTag(jimmUUID string, db *db.Database, tag string) (*ofganames.Tag, e if err != nil { return nil, errors.E(fmt.Sprintf("group %s not found", trailer)) } - return ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(strconv.FormatUint(uint64(entry.ID), 10)), relation), nil + return ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(entry.UUID), relation), nil case names.ControllerTagKind: zapctx.Debug( diff --git a/internal/jimm/access_test.go b/internal/jimm/access_test.go index 369eba708..cda526635 100644 --- a/internal/jimm/access_test.go +++ b/internal/jimm/access_test.go @@ -638,7 +638,7 @@ func TestResolveTupleObjectMapsGroups(t *testing.T) { c.Assert(err, qt.IsNil) tag, err := jimm.ResolveTag(j.UUID, &j.Database, "group-"+group.Name+"#member") c.Assert(err, qt.IsNil) - c.Assert(tag, qt.DeepEquals, ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag("1"), ofganames.MemberRelation)) + c.Assert(tag, qt.DeepEquals, ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(group.UUID), ofganames.MemberRelation)) } func TestResolveTagObjectMapsUsers(t *testing.T) { diff --git a/internal/jujuapi/access_control_test.go b/internal/jujuapi/access_control_test.go index 9d1b02fb1..87ab92ddb 100644 --- a/internal/jujuapi/access_control_test.go +++ b/internal/jujuapi/access_control_test.go @@ -307,7 +307,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { want: createTuple( "user:"+user.Name, "member", - "group:"+stringGroupID(group.ID), + "group:"+group.UUID, ), err: false, changesType: "group", @@ -318,7 +318,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { want: createTuple( "user:"+"kelvin.lina.test@canonical.com", "member", - "group:"+stringGroupID(group.ID), + "group:"+group.UUID, ), err: false, changesType: "group", @@ -327,7 +327,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { { input: tuple{"group-" + "test-group#member", "administrator", "controller-" + controller.UUID}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "administrator", "controller:"+controller.UUID, ), @@ -382,7 +382,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { { input: tuple{"group-" + group.Name + "#member", "administrator", "controller-" + controller.Name}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "administrator", "controller:"+controller.UUID, ), @@ -393,7 +393,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { { input: tuple{"group-" + group.Name + "#member", "administrator", "controller-" + controller.UUID}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "administrator", "controller:"+controller.UUID, ), @@ -404,7 +404,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { { input: tuple{"group-" + group.Name + "#member", "writer", "model-" + controller.Name + ":" + user.Name + "/" + model.Name}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "writer", "model:"+model.UUID.String, ), @@ -415,7 +415,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { { input: tuple{"group-" + group.Name + "#member", "writer", "model-" + model.UUID.String}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "writer", "model:"+model.UUID.String, ), @@ -426,7 +426,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { { input: tuple{"group-" + group.Name + "#member", "consumer", "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "consumer", "applicationoffer:"+offer.UUID, ), @@ -437,7 +437,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { { input: tuple{"group-" + group.Name + "#member", "consumer", "applicationoffer-" + offer.UUID}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "consumer", "applicationoffer:"+offer.UUID, ), @@ -448,9 +448,9 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { { input: tuple{"group-" + group.Name + "#member", "member", "group-" + group2.Name}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "member", - "group:"+stringGroupID(group2.ID), + "group:"+group2.UUID, ), err: false, changesType: "group", @@ -566,7 +566,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { want: createTuple( "user:"+user.Name, "member", - "group:"+stringGroupID(group.ID), + "group:"+group.UUID, ), err: false, changesType: "group", @@ -580,7 +580,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { }, toRemove: tuple{"group-" + group.Name + "#member", "administrator", "controller-" + controller.UUID}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "administrator", "controller:"+controller.UUID, ), @@ -660,7 +660,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { }, toRemove: tuple{"group-" + group.Name + "#member", "administrator", "controller-" + controller.Name}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "administrator", "controller:"+controller.UUID, ), @@ -676,7 +676,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { }, toRemove: tuple{"group-" + group.Name + "#member", "administrator", "controller-" + controller.UUID}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "administrator", "controller:"+controller.UUID, ), @@ -692,7 +692,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { }, toRemove: tuple{"group-" + group.Name + "#member", "writer", "model-" + controller.Name + ":" + user.Name + "/" + model.Name}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "writer", "model:"+model.UUID.String, ), @@ -708,7 +708,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { }, toRemove: tuple{"group-" + group.Name + "#member", "writer", "model-" + model.UUID.String}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "writer", "model:"+model.UUID.String, ), @@ -724,7 +724,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { }, toRemove: tuple{"group-" + group.Name + "#member", "consumer", "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "consumer", "applicationoffer:"+offer.UUID, ), @@ -740,7 +740,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { }, toRemove: tuple{"group-" + group.Name + "#member", "consumer", "applicationoffer-" + offer.UUID}, want: createTuple( - "group:"+stringGroupID(group.ID)+"#member", + "group:"+group.UUID+"#member", "consumer", "applicationoffer:"+offer.UUID, ), diff --git a/internal/openfga/names/names_test.go b/internal/openfga/names/names_test.go index 5a2b2736a..c58a04d4c 100644 --- a/internal/openfga/names/names_test.go +++ b/internal/openfga/names/names_test.go @@ -42,8 +42,8 @@ func (s *namesSuite) TestFromResourceTag(c *gc.C) { result = ofganames.ConvertTag(names.NewCloudTag("test")) c.Assert(result, gc.DeepEquals, ofganames.NewTag("test", names.CloudTagKind, "")) - result = ofganames.ConvertTag(jimmnames.NewGroupTag("1")) - c.Assert(result, gc.DeepEquals, ofganames.NewTag("1", jimmnames.GroupTagKind, "")) + result = ofganames.ConvertTag(jimmnames.NewGroupTag(id.String())) + c.Assert(result, gc.DeepEquals, ofganames.NewTag(id.String(), jimmnames.GroupTagKind, "")) } func (s *namesSuite) TestFromGenericResourceTag(c *gc.C) { diff --git a/internal/openfga/openfga_test.go b/internal/openfga/openfga_test.go index f5dd8e479..dd4fbdc4b 100644 --- a/internal/openfga/openfga_test.go +++ b/internal/openfga/openfga_test.go @@ -36,14 +36,14 @@ func (s *openFGATestSuite) SetUpTest(c *gc.C) { func (s *openFGATestSuite) TestWritingTuplesToOFGASucceeds(c *gc.C) { ctx := context.Background() - groupid := "1" + groupUUID := uuid.NewString() uuid1, _ := uuid.NewRandom() user1 := names.NewUserTag(uuid1.String()) tuple1 := openfga.Tuple{ Object: ofganames.ConvertTag(user1), Relation: "member", - Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupid)), + Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), } uuid2, _ := uuid.NewRandom() @@ -51,7 +51,7 @@ func (s *openFGATestSuite) TestWritingTuplesToOFGASucceeds(c *gc.C) { tuple2 := openfga.Tuple{ Object: ofganames.ConvertTag(user2), Relation: "member", - Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupid)), + Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), } err := s.ofgaClient.AddRelation(ctx, tuple1, tuple2) @@ -69,21 +69,21 @@ func (s *openFGATestSuite) TestWritingTuplesToOFGASucceeds(c *gc.C) { func (suite *openFGATestSuite) TestRemovingTuplesFromOFGASucceeds(c *gc.C) { ctx := context.Background() - groupid := "2" + groupUUID := uuid.NewString() //Create tuples before writing to db user1 := ofganames.ConvertTag(names.NewUserTag("bob")) tuple1 := openfga.Tuple{ Object: user1, Relation: "member", - Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupid)), + Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), } user2 := ofganames.ConvertTag(names.NewUserTag("alice")) tuple2 := openfga.Tuple{ Object: user2, Relation: "member", - Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupid)), + Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), } //Delete before insert should fail @@ -113,7 +113,7 @@ func (suite *openFGATestSuite) TestRemovingTuplesFromOFGASucceeds(c *gc.C) { func (s *openFGATestSuite) TestCheckRelationSucceeds(c *gc.C) { ctx := context.Background() - groupid := "3" + groupUUID := uuid.NewString() controllerUUID, _ := uuid.NewRandom() controller := names.NewControllerTag(controllerUUID.String()) @@ -121,10 +121,10 @@ func (s *openFGATestSuite) TestCheckRelationSucceeds(c *gc.C) { userToGroup := openfga.Tuple{ Object: user, Relation: "member", - Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupid)), + Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), } groupToController := openfga.Tuple{ - Object: ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(groupid), ofganames.MemberRelation), + Object: ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(groupUUID), ofganames.MemberRelation), Relation: "administrator", Target: ofganames.ConvertTag(controller), } @@ -143,7 +143,7 @@ func (s *openFGATestSuite) TestCheckRelationSucceeds(c *gc.C) { } func (s *openFGATestSuite) TestRemoveTuplesSucceeds(c *gc.C) { - groupid := "4" + groupUUID := uuid.NewString() // Note (babakks): OpenFGA only supports a limited number of write operation // per request (default is 100). That's why we're testing with a large number @@ -155,14 +155,14 @@ func (s *openFGATestSuite) TestRemoveTuplesSucceeds(c *gc.C) { tuple := openfga.Tuple{ Object: ofganames.ConvertTag(names.NewUserTag("test" + strconv.Itoa(i))), Relation: "member", - Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupid)), + Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), } err := s.ofgaClient.AddRelation(context.Background(), tuple) c.Assert(err, gc.IsNil) } checkTuple := openfga.Tuple{ - Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupid)), + Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), } c.Logf("checking for tuple %v\n", checkTuple) err := s.ofgaClient.RemoveTuples(context.Background(), checkTuple) @@ -277,8 +277,8 @@ func (s *openFGATestSuite) TestRemoveApplicationOffer(c *gc.C) { } func (s *openFGATestSuite) TestRemoveGroup(c *gc.C) { - group1 := jimmnames.NewGroupTag("1") - group2 := jimmnames.NewGroupTag("2") + group1 := jimmnames.NewGroupTag(uuid.NewString()) + group2 := jimmnames.NewGroupTag(uuid.NewString()) alice := names.NewUserTag("alice@canonical.com") adam := names.NewUserTag("adam@canonical.com") @@ -448,6 +448,8 @@ func (s *openFGATestSuite) TestListObjectsWithContextualTuples(c *gc.C) { } } + groupUUID := uuid.NewString() + ids, err := s.ofgaClient.ListObjects(ctx, ofganames.ConvertTag(names.NewUserTag("alice")), "reader", "model", []openfga.Tuple{ { Object: ofganames.ConvertTag(names.NewUserTag("alice")), @@ -458,10 +460,10 @@ func (s *openFGATestSuite) TestListObjectsWithContextualTuples(c *gc.C) { { Object: ofganames.ConvertTag(names.NewUserTag("alice")), Relation: ofganames.MemberRelation, - Target: ofganames.ConvertTag(jimmnames.NewGroupTag("1")), + Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), }, { - Object: ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag("1"), ofganames.MemberRelation), + Object: ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(groupUUID), ofganames.MemberRelation), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag(modelUUIDs[1])), }, @@ -505,6 +507,8 @@ func (s *openFGATestSuite) TestListObjectsWithPeristedTuples(c *gc.C) { } } + groupUUID := uuid.NewString() + c.Assert(s.ofgaClient.AddRelation(ctx, []openfga.Tuple{ { @@ -516,10 +520,10 @@ func (s *openFGATestSuite) TestListObjectsWithPeristedTuples(c *gc.C) { { Object: ofganames.ConvertTag(names.NewUserTag("alice")), Relation: ofganames.MemberRelation, - Target: ofganames.ConvertTag(jimmnames.NewGroupTag("1")), + Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), }, { - Object: ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag("1"), ofganames.MemberRelation), + Object: ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(groupUUID), ofganames.MemberRelation), Relation: ofganames.ReaderRelation, Target: ofganames.ConvertTag(names.NewModelTag(modelUUIDs[1])), }, diff --git a/internal/openfga/user_test.go b/internal/openfga/user_test.go index ea03167b1..a94691d9a 100644 --- a/internal/openfga/user_test.go +++ b/internal/openfga/user_test.go @@ -34,7 +34,7 @@ func (s *userTestSuite) SetUpTest(c *gc.C) { func (s *userTestSuite) TestIsAdministrator(c *gc.C) { ctx := context.Background() - groupid := "3" + groupUUID := uuid.NewString() controllerUUID, _ := uuid.NewRandom() controller := names.NewControllerTag(controllerUUID.String()) @@ -42,10 +42,10 @@ func (s *userTestSuite) TestIsAdministrator(c *gc.C) { userToGroup := openfga.Tuple{ Object: ofganames.ConvertTag(user), Relation: "member", - Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupid)), + Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), } groupToController := openfga.Tuple{ - Object: ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(groupid), ofganames.MemberRelation), + Object: ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(groupUUID), ofganames.MemberRelation), Relation: "administrator", Target: ofganames.ConvertTag(controller), } @@ -68,8 +68,8 @@ func (s *userTestSuite) TestIsAdministrator(c *gc.C) { func (s *userTestSuite) TestModelAccess(c *gc.C) { ctx := context.Background() - groupid := "3" - group := jimmnames.NewGroupTag(groupid) + groupUUID := uuid.NewString() + group := jimmnames.NewGroupTag(groupUUID) controllerUUID, err := uuid.NewRandom() c.Assert(err, gc.IsNil) @@ -85,7 +85,7 @@ func (s *userTestSuite) TestModelAccess(c *gc.C) { tuples := []openfga.Tuple{{ Object: ofganames.ConvertTag(eve), Relation: ofganames.MemberRelation, - Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupid)), + Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), }, { Object: ofganames.ConvertTagWithRelation(group, ofganames.MemberRelation), Relation: ofganames.AdministratorRelation, @@ -177,8 +177,8 @@ func (s *userTestSuite) TestSetModelAccess(c *gc.C) { func (s *userTestSuite) TestCloudAccess(c *gc.C) { ctx := context.Background() - groupid := "3" - group := jimmnames.NewGroupTag(groupid) + groupUUID := uuid.NewString() + group := jimmnames.NewGroupTag(groupUUID) controllerUUID, err := uuid.NewRandom() c.Assert(err, gc.IsNil) @@ -194,7 +194,7 @@ func (s *userTestSuite) TestCloudAccess(c *gc.C) { tuples := []openfga.Tuple{{ Object: ofganames.ConvertTag(eve), Relation: ofganames.MemberRelation, - Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupid)), + Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), }, { Object: ofganames.ConvertTagWithRelation(group, ofganames.MemberRelation), Relation: ofganames.AdministratorRelation, @@ -274,8 +274,8 @@ func (s *userTestSuite) TestSetCloudAccess(c *gc.C) { func (s *userTestSuite) TestControllerAccess(c *gc.C) { ctx := context.Background() - groupid := "3" - group := jimmnames.NewGroupTag(groupid) + groupUUID := uuid.NewString() + group := jimmnames.NewGroupTag(groupUUID) controllerUUID, err := uuid.NewRandom() c.Assert(err, gc.IsNil) @@ -287,7 +287,7 @@ func (s *userTestSuite) TestControllerAccess(c *gc.C) { tuples := []openfga.Tuple{{ Object: ofganames.ConvertTag(eve), Relation: ofganames.MemberRelation, - Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupid)), + Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), }, { Object: ofganames.ConvertTagWithRelation(group, ofganames.MemberRelation), Relation: ofganames.AdministratorRelation, @@ -411,11 +411,11 @@ func (s *userTestSuite) TestUnsetAuditLogViewerAccess(c *gc.C) { func (s *userTestSuite) TestListRelatedUsers(c *gc.C) { ctx := context.Background() - groupid := "3" - group := jimmnames.NewGroupTag(groupid) + groupUUID := uuid.NewString() + group := jimmnames.NewGroupTag(groupUUID) - groupid2 := "4" - group2 := jimmnames.NewGroupTag(groupid2) + groupUUID2 := uuid.NewString() + group2 := jimmnames.NewGroupTag(groupUUID2) controllerUUID, err := uuid.NewRandom() c.Assert(err, gc.IsNil) @@ -436,7 +436,7 @@ func (s *userTestSuite) TestListRelatedUsers(c *gc.C) { tuples := []openfga.Tuple{{ Object: ofganames.ConvertTag(eve), Relation: ofganames.MemberRelation, - Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupid)), + Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), }, { Object: ofganames.ConvertTagWithRelation(group, ofganames.MemberRelation), Relation: ofganames.AdministratorRelation, diff --git a/pkg/names/group.go b/pkg/names/group.go index 839c04d9e..24ca1f4b3 100644 --- a/pkg/names/group.go +++ b/pkg/names/group.go @@ -11,7 +11,7 @@ const ( var ( validGroupName = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9._-]{4,}[a-zA-Z0-9]$") - validGroupIdSnippet = `^[1-9][0-9]*(#|\z)[a-z]*$` + validGroupIdSnippet = `^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}(#|\z)[a-z]*$` validGroupId = regexp.MustCompile(validGroupIdSnippet) ) diff --git a/pkg/names/group_test.go b/pkg/names/group_test.go index 0983c0054..75ee5de74 100644 --- a/pkg/names/group_test.go +++ b/pkg/names/group_test.go @@ -1,28 +1,32 @@ package names import ( + "fmt" "regexp" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" ) func TestParseGroupTagAcceptsGroups(t *testing.T) { - gt, err := ParseGroupTag("group-1") + uuid := uuid.NewString() + gt, err := ParseGroupTag(fmt.Sprintf("group-%s", uuid)) assert.NoError(t, err) - assert.Equal(t, "1", gt.id) - assert.Equal(t, "1", gt.Id()) + assert.Equal(t, uuid, gt.id) + assert.Equal(t, uuid, gt.Id()) assert.Equal(t, "group", gt.Kind()) - assert.Equal(t, "group-1", gt.String()) + assert.Equal(t, fmt.Sprintf("group-%s", uuid), gt.String()) } func TestParseGroupTagAcceptsGroupsWithRelationSpecifier(t *testing.T) { - gt, err := ParseGroupTag("group-1#member") + uuid := uuid.NewString() + gt, err := ParseGroupTag(fmt.Sprintf("group-%s#member", uuid)) assert.NoError(t, err) - assert.Equal(t, "1#member", gt.id) - assert.Equal(t, "1#member", gt.Id()) + assert.Equal(t, fmt.Sprintf("%s#member", uuid), gt.id) + assert.Equal(t, fmt.Sprintf("%s#member", uuid), gt.Id()) assert.Equal(t, "group", gt.Kind()) - assert.Equal(t, "group-1#member", gt.String()) + assert.Equal(t, fmt.Sprintf("group-%s#member", uuid), gt.String()) } func TestParseGroupTagDeniesBadKinds(t *testing.T) { @@ -97,6 +101,9 @@ func TestIsValidGroupName(t *testing.T) { }, { name: "short_", expectedValidity: false, + }, { + name: "group.A#member", + expectedValidity: false, }} for _, test := range tests { From 872f69aa71abaf9fb8946c591fc6f01009daf541 Mon Sep 17 00:00:00 2001 From: Ales Stimec Date: Wed, 15 May 2024 08:10:57 +0200 Subject: [PATCH 123/126] Refactors tests in pkg/names. --- cmd/jaas/cmd/grant_test.go | 9 ++- pkg/names/applicationoffer.go | 2 + pkg/names/group.go | 4 +- pkg/names/group_test.go | 108 ++++++++++++++++++----------- pkg/names/names.go | 2 + pkg/names/service_account.go | 7 +- pkg/names/service_account_test.go | 110 +++++++++++++++++++++--------- 7 files changed, 163 insertions(+), 79 deletions(-) diff --git a/cmd/jaas/cmd/grant_test.go b/cmd/jaas/cmd/grant_test.go index a9399f00b..a40999a24 100644 --- a/cmd/jaas/cmd/grant_test.go +++ b/cmd/jaas/cmd/grant_test.go @@ -4,6 +4,7 @@ package cmd_test import ( "context" + "fmt" "github.com/juju/cmd/v3/cmdtesting" "github.com/juju/names/v5" @@ -51,6 +52,12 @@ func (s *grantSuite) TestGrant(c *gc.C) { err = s.JIMM.Database.AddGroup(ctx, "1") c.Assert(err, gc.IsNil) + group := dbmodel.GroupEntry{ + Name: "1", + } + err = s.JIMM.Database.GetGroup(ctx, &group) + c.Assert(err, gc.IsNil) + cmdContext, err := cmdtesting.RunCommand(c, cmd.NewGrantCommandForTesting(s.ClientStore(), bClient), clientID, "user-bob", "group-1") c.Assert(err, gc.IsNil) c.Assert(cmdtesting.Stdout(cmdContext), gc.Equals, "access granted\n") @@ -64,7 +71,7 @@ func (s *grantSuite) TestGrant(c *gc.C) { c.Assert(ok, gc.Equals, true) ok, err = s.JIMM.OpenFGAClient.CheckRelation(ctx, openfga.Tuple{ - Object: ofganames.ConvertTag(jimmnames.NewGroupTag("1#member")), + Object: ofganames.ConvertTag(jimmnames.NewGroupTag(fmt.Sprintf("%s#member", group.UUID))), Relation: ofganames.AdministratorRelation, Target: ofganames.ConvertTag(jimmnames.NewServiceAccountTag(clientIdWithDomain)), }, false) diff --git a/pkg/names/applicationoffer.go b/pkg/names/applicationoffer.go index 9e3fa1da4..e4a163063 100644 --- a/pkg/names/applicationoffer.go +++ b/pkg/names/applicationoffer.go @@ -1,3 +1,5 @@ +// Copyright 2024 Canonical Ltd. + package names import ( diff --git a/pkg/names/group.go b/pkg/names/group.go index 24ca1f4b3..6d9f4f066 100644 --- a/pkg/names/group.go +++ b/pkg/names/group.go @@ -1,3 +1,5 @@ +// Copyright 2024 Canonical Ltd. + package names import ( @@ -11,7 +13,7 @@ const ( var ( validGroupName = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9._-]{4,}[a-zA-Z0-9]$") - validGroupIdSnippet = `^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}(#|\z)[a-z]*$` + validGroupIdSnippet = `^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}((#|\z)[a-z]+)?$` validGroupId = regexp.MustCompile(validGroupIdSnippet) ) diff --git a/pkg/names/group_test.go b/pkg/names/group_test.go index 75ee5de74..1a495889b 100644 --- a/pkg/names/group_test.go +++ b/pkg/names/group_test.go @@ -1,61 +1,89 @@ -package names +// Copyright 2024 Canonical Ltd. + +package names_test import ( "fmt" - "regexp" "testing" + qt "github.com/frankban/quicktest" "github.com/google/uuid" "github.com/stretchr/testify/assert" + + "github.com/canonical/jimm/pkg/names" ) -func TestParseGroupTagAcceptsGroups(t *testing.T) { +func TestParseGroupTag(t *testing.T) { + c := qt.New(t) uuid := uuid.NewString() - gt, err := ParseGroupTag(fmt.Sprintf("group-%s", uuid)) - assert.NoError(t, err) - assert.Equal(t, uuid, gt.id) - assert.Equal(t, uuid, gt.Id()) - assert.Equal(t, "group", gt.Kind()) - assert.Equal(t, fmt.Sprintf("group-%s", uuid), gt.String()) -} -func TestParseGroupTagAcceptsGroupsWithRelationSpecifier(t *testing.T) { - uuid := uuid.NewString() - gt, err := ParseGroupTag(fmt.Sprintf("group-%s#member", uuid)) - assert.NoError(t, err) - assert.Equal(t, fmt.Sprintf("%s#member", uuid), gt.id) - assert.Equal(t, fmt.Sprintf("%s#member", uuid), gt.Id()) - assert.Equal(t, "group", gt.Kind()) - assert.Equal(t, fmt.Sprintf("group-%s#member", uuid), gt.String()) + tests := []struct { + tag string + expectedError string + expectedTag string + expectedId string + }{{ + tag: fmt.Sprintf("group-%s", uuid), + expectedId: uuid, + expectedTag: fmt.Sprintf("group-%s", uuid), + }, { + tag: fmt.Sprintf("group-%s#member", uuid), + expectedId: fmt.Sprintf("%s#member", uuid), + expectedTag: fmt.Sprintf("group-%s#member", uuid), + }, { + tag: "pokemon-diglett", + expectedError: "\"pokemon-diglett\" is not a valid tag", + }} + + for i, test := range tests { + test := test + c.Run(fmt.Sprintf("test case %d", i), func(c *qt.C) { + gt, err := names.ParseGroupTag(test.tag) + if test.expectedError == "" { + c.Assert(err, qt.IsNil) + c.Assert(gt.Id(), qt.Equals, test.expectedId) + c.Assert(gt.Kind(), qt.Equals, "group") + c.Assert(gt.String(), qt.Equals, test.expectedTag) + } else { + c.Assert(err, qt.ErrorMatches, test.expectedError) + } + }) + } } func TestParseGroupTagDeniesBadKinds(t *testing.T) { - _, err := ParseGroupTag("pokemon-diglett") + _, err := names.ParseGroupTag("pokemon-diglett") assert.Error(t, err) assert.ErrorContains(t, err, "\"pokemon-diglett\" is not a valid tag") } func TestIsValidGroupId(t *testing.T) { - r := regexp.MustCompile(`^[1-9][0-9]*(#|\z)[a-z]*$`) - assert.False(t, r.MatchString("0#hi")) - assert.False(t, r.MatchString("0")) - assert.False(t, r.MatchString("a#hi")) - assert.False(t, r.MatchString("01")) - assert.True(t, r.MatchString("1")) - assert.True(t, r.MatchString("1#")) - assert.True(t, r.MatchString("1#hi")) - - s := r.FindString("1010") - assert.Equal(t, "1010", s) - - s = r.FindString("01010") - assert.Equal(t, "", s) - - s = r.FindString("01010#") - assert.Equal(t, "", s) - - s = r.FindString("1010#member") - assert.Equal(t, "1010#member", s) + uuid := uuid.NewString() + tests := []struct { + id string + expectedValid bool + }{{ + id: uuid, + expectedValid: true, + }, { + id: fmt.Sprintf("%s#member", uuid), + expectedValid: true, + }, { + id: fmt.Sprintf("%s#member#member", uuid), + expectedValid: false, + }, { + id: fmt.Sprintf("%s#", uuid), + expectedValid: false, + }, { + id: "0#member", + expectedValid: false, + }, { + id: "0", + expectedValid: false, + }} + for _, test := range tests { + assert.Equal(t, names.IsValidGroupId(test.id), test.expectedValid) + } } func TestIsValidGroupName(t *testing.T) { @@ -109,7 +137,7 @@ func TestIsValidGroupName(t *testing.T) { for _, test := range tests { t.Logf("testing group name %q, expected validity %v", test.name, test.expectedValidity) - valid := IsValidGroupName(test.name) + valid := names.IsValidGroupName(test.name) assert.Equal(t, valid, test.expectedValidity) } } diff --git a/pkg/names/names.go b/pkg/names/names.go index 37ad1f661..84e61ea98 100644 --- a/pkg/names/names.go +++ b/pkg/names/names.go @@ -1,3 +1,5 @@ +// Copyright 2024 Canonical Ltd. + package names import ( diff --git a/pkg/names/service_account.go b/pkg/names/service_account.go index e4575c7fc..dd4102f06 100644 --- a/pkg/names/service_account.go +++ b/pkg/names/service_account.go @@ -1,7 +1,5 @@ -// Copyright 2024 canonical. +// Copyright 2024 Canonical Ltd. -// Service accounts are an OIDC/OAuth concept which allows for machine<->machine communication. -// Service accounts are identified by their client ID. package names import ( @@ -22,6 +20,9 @@ const ( ServiceAccountDomain = "serviceaccount" ) +// Service accounts are an OIDC/OAuth concept which allows for machine<->machine communication. +// Service accounts are identified by their client ID. + // ServiceAccount represents a service account where id is the client ID. // Implements juju names.Tag. type ServiceAccountTag struct { diff --git a/pkg/names/service_account_test.go b/pkg/names/service_account_test.go index 445c2a3ca..74dc707d1 100644 --- a/pkg/names/service_account_test.go +++ b/pkg/names/service_account_test.go @@ -1,13 +1,18 @@ -package names +// Copyright 2024 Canonical Ltd. + +package names_test import ( + "fmt" "testing" qt "github.com/frankban/quicktest" - "github.com/stretchr/testify/assert" + + "github.com/canonical/jimm/pkg/names" ) func TestParseServiceAccountID(t *testing.T) { + c := qt.New(t) tests := []struct { about string tag string @@ -17,54 +22,91 @@ func TestParseServiceAccountID(t *testing.T) { about: "Valid svc account tag", tag: "serviceaccount-1e654457-a195-4a41-8360-929c7f455d43@serviceaccount", expectedID: "1e654457-a195-4a41-8360-929c7f455d43@serviceaccount", - err: "", }, { about: "Invalid svc account tag (no domain)", tag: "serviceaccount-1e654457-a195-4a41-8360-929c7f455d43", - err: "is not a valid serviceaccount tag", + err: ".*is not a valid serviceaccount tag", }, { about: "Invalid svc account tag (serviceaccounts)", tag: "serviceaccounts-1e654457-a195-4a41-8360-929c7f455d43@serviceaccount", - err: "is not a valid tag", + err: ".*is not a valid tag", }, { about: "Invalid svc account tag (no prefix)", tag: "1e654457-a195-4a41-8360-929c7f455d43@serviceaccount", - err: "is not a valid tag", + err: ".*is not a valid tag", }, { about: "Invalid svc account tag (missing ID)", tag: "serviceaccounts-", - err: "is not a valid tag", + err: ".*is not a valid tag", }} for _, test := range tests { - t.Run(test.about, func(t *testing.T) { - gt, err := ParseServiceAccountTag(test.tag) + test := test + c.Run(test.about, func(c *qt.C) { + gt, err := names.ParseServiceAccountTag(test.tag) if test.err == "" { - assert.NoError(t, err) - assert.Equal(t, test.expectedID, gt.id) - assert.Equal(t, test.expectedID, gt.Id()) - assert.Equal(t, "serviceaccount", gt.Kind()) - assert.Equal(t, test.tag, gt.String()) + c.Assert(err, qt.IsNil) + c.Assert(gt.Id(), qt.Equals, test.expectedID) + c.Assert(gt.Kind(), qt.Equals, "serviceaccount") + c.Assert(gt.String(), qt.Equals, test.tag) } else { - assert.ErrorContains(t, err, test.err) + c.Assert(err, qt.ErrorMatches, test.err) } }) } } func TestIsValidServiceAccountId(t *testing.T) { - assert.True(t, IsValidServiceAccountId("1e654457-a195-4a41-8360-929c7f455d43@serviceaccount")) - assert.True(t, IsValidServiceAccountId("12345@serviceaccount")) - assert.True(t, IsValidServiceAccountId("abc123@serviceaccount")) - assert.True(t, IsValidServiceAccountId("ABC123@serviceaccount")) - assert.True(t, IsValidServiceAccountId("ABC123@serviceaccount")) - assert.False(t, IsValidServiceAccountId("ABC123")) - assert.False(t, IsValidServiceAccountId("abc 123")) - assert.False(t, IsValidServiceAccountId("")) - assert.False(t, IsValidServiceAccountId(" ")) - assert.False(t, IsValidServiceAccountId("@")) - assert.False(t, IsValidServiceAccountId("@serviceaccount")) - assert.False(t, IsValidServiceAccountId("abc123@some-other-domain")) - assert.False(t, IsValidServiceAccountId("abc123@")) + c := qt.New(t) + tests := []struct { + id string + expectedValid bool + }{{ + id: "1e654457-a195-4a41-8360-929c7f455d43@serviceaccount", + expectedValid: true, + }, { + id: "12345@serviceaccount", + expectedValid: true, + }, { + id: "abc123@serviceaccount", + expectedValid: true, + }, { + id: "ABC123@serviceaccount", + expectedValid: true, + }, { + id: "ABC123@serviceaccount", + expectedValid: true, + }, { + id: "ABC123", + expectedValid: false, + }, { + id: "abc 123", + expectedValid: false, + }, { + id: "", + expectedValid: false, + }, { + id: " ", + expectedValid: false, + }, { + id: "@", + expectedValid: false, + }, { + id: "@serviceaccount", + expectedValid: false, + }, { + id: "abc123@some-other-domain", + expectedValid: false, + }, { + id: "abc123@", + expectedValid: false, + }} + for i, test := range tests { + test := test + c.Run(fmt.Sprintf("test case %d", i), func(c *qt.C) { + c.Assert(names.IsValidServiceAccountId(test.id), qt.Equals, test.expectedValid) + }) + } + } func TestEnsureValidClientIdWithDomain(t *testing.T) { @@ -102,16 +144,16 @@ func TestEnsureValidClientIdWithDomain(t *testing.T) { }, } - for _, t := range tests { - tt := t - c.Run(tt.name, func(c *qt.C) { - result, err := EnsureValidServiceAccountId(tt.id) - if tt.expectedError { + for _, test := range tests { + test := test + c.Run(test.name, func(c *qt.C) { + result, err := names.EnsureValidServiceAccountId(test.id) + if test.expectedError { c.Assert(err, qt.ErrorMatches, "invalid client ID") c.Assert(result, qt.Equals, "") } else { c.Assert(err, qt.IsNil) - c.Assert(result, qt.Equals, tt.expectedId) + c.Assert(result, qt.Equals, test.expectedId) } }) } From df1880450056097341605969421c9e63c2283986 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 17 May 2024 09:10:55 +0200 Subject: [PATCH 124/126] Use Vault KV2 (#1214) * Use vault kv2 store * Fix tests and replicate previous behavior * Further test fixes --- Makefile | 2 +- internal/jimmtest/vault.go | 2 +- internal/vault/vault.go | 126 ++++++++++++++---------------- internal/vault/vault_test.go | 2 +- internal/wellknownapi/api_test.go | 2 +- local/vault/init.sh | 2 +- service.go | 2 +- 7 files changed, 65 insertions(+), 73 deletions(-) diff --git a/Makefile b/Makefile index 582360578..f2d2d7004 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ dev-env-setup: sysdeps certs @touch ./local/vault/approle.json && touch ./local/vault/roleid.txt && touch ./local/vault/vault.env @make version/commit.txt && make version/version.txt -dev-env: +dev-env: dev-env-setup @docker compose --profile dev up --force-recreate dev-env-cleanup: diff --git a/internal/jimmtest/vault.go b/internal/jimmtest/vault.go index aefdf73b5..0b36c302c 100644 --- a/internal/jimmtest/vault.go +++ b/internal/jimmtest/vault.go @@ -49,5 +49,5 @@ func VaultClient(tb fatalF, prefix string) (*api.Client, string, string, string, if !ok { panic("failed to convert role secret ID to string") } - return vaultClient, "/jimm-kv/", roleIDString, roleSecretIDString, true + return vaultClient, "jimm-kv", roleIDString, roleSecretIDString, true } diff --git a/internal/vault/vault.go b/internal/vault/vault.go index e8618bae2..52698b578 100644 --- a/internal/vault/vault.go +++ b/internal/vault/vault.go @@ -6,6 +6,7 @@ import ( "context" "encoding/base64" "encoding/json" + goerr "errors" "net/http" "path" "sync" @@ -25,6 +26,13 @@ const ( passwordKey = "password" ) +const ( + jwksKey = "jwks" + jwksExpiryKey = "jwks-expiry" + jwksPrivateKey = "jwks-private" + oAuthSecretKey = "oauth-secret" +) + // A VaultStore stores cloud credential attributes and // controller credentials in vault. type VaultStore struct { @@ -58,11 +66,11 @@ func (s *VaultStore) Get(ctx context.Context, tag names.CloudCredentialTag) (map return nil, errors.E(op, err) } - secret, err := client.Logical().ReadWithContext(ctx, s.path(tag)) - if err != nil { + secret, err := client.KVv2(s.KVPath).Get(ctx, s.path(tag)) + if err != nil && goerr.Unwrap(err) != api.ErrSecretNotFound { return nil, errors.E(op, err) } - if secret == nil { + if secret == nil || secret.Data == nil { return nil, nil } attr := make(map[string]string, len(secret.Data)) @@ -95,7 +103,7 @@ func (s *VaultStore) Put(ctx context.Context, tag names.CloudCredentialTag, attr for k, v := range attr { data[k] = v } - _, err = client.Logical().WriteWithContext(ctx, s.path(tag), data) + _, err = client.KVv2(s.KVPath).Put(ctx, s.path(tag), data) if err != nil { return errors.E(op, err) } @@ -111,7 +119,7 @@ func (s *VaultStore) delete(ctx context.Context, tag names.CloudCredentialTag) e if err != nil { return errors.E(op, err) } - _, err = client.Logical().DeleteWithContext(ctx, s.path(tag)) + err = client.KVv2(s.KVPath).Delete(ctx, s.path(tag)) if rerr, ok := err.(*api.ResponseError); ok && rerr.StatusCode == http.StatusNotFound { // Ignore the error if attempting to delete something that isn't there. err = nil @@ -132,11 +140,11 @@ func (s *VaultStore) GetControllerCredentials(ctx context.Context, controllerNam return "", "", errors.E(op, err) } - secret, err := client.Logical().ReadWithContext(ctx, s.controllerCredentialsPath(controllerName)) - if err != nil { + secret, err := client.KVv2(s.KVPath).Get(ctx, s.controllerCredentialsPath(controllerName)) + if err != nil && goerr.Unwrap(err) != api.ErrSecretNotFound { return "", "", errors.E(op, err) } - if secret == nil { + if secret == nil || secret.Data == nil { return "", "", nil } var username, password string @@ -168,7 +176,7 @@ func (s *VaultStore) PutControllerCredentials(ctx context.Context, controllerNam usernameKey: username, passwordKey: password, } - _, err = client.Logical().WriteWithContext(ctx, s.controllerCredentialsPath(controllerName), data) + _, err = client.KVv2(s.KVPath).Put(ctx, s.controllerCredentialsPath(controllerName), data) if err != nil { return errors.E(op, err) } @@ -185,9 +193,9 @@ func (s *VaultStore) CleanupJWKS(ctx context.Context) error { } // Vault does not return errors on deletion requests where // the secret does not exist. As such we just return the last known error. - client.Logical().DeleteWithContext(ctx, s.getJWKSExpiryPath()) - client.Logical().DeleteWithContext(ctx, s.getJWKSPath()) - if _, err = client.Logical().DeleteWithContext(ctx, s.getJWKSPrivateKeyPath()); err != nil { + client.KVv2(s.KVPath).Delete(ctx, s.getJWKSExpiryPath()) + client.KVv2(s.KVPath).Delete(ctx, s.getJWKSPath()) + if err = client.KVv2(s.KVPath).Delete(ctx, s.getJWKSPrivateKeyPath()); err != nil { return errors.E(op, err) } return nil @@ -202,26 +210,26 @@ func (s *VaultStore) GetJWKS(ctx context.Context) (jwk.Set, error) { return nil, errors.E(op, err) } - secret, err := client.Logical().ReadWithContext(ctx, s.getJWKSPath()) - if err != nil { + secret, err := client.KVv2(s.KVPath).Get(ctx, s.getJWKSPath()) + if err != nil && goerr.Unwrap(err) != api.ErrSecretNotFound { return nil, errors.E(op, err) } // This is how the current version of vaults API Read works, // if the secret is not present on the path, it will instead return // nil for the secret and a nil error. So we must check for this. - if secret == nil { + if secret == nil || secret.Data == nil { msg := "no JWKS exists yet." zapctx.Debug(ctx, msg) return nil, errors.E(op, errors.CodeNotFound, msg) } - b, err := json.Marshal(secret.Data) - if err != nil { - return nil, errors.E(op, err) + jsonString, ok := secret.Data[jwksKey].(string) + if !ok { + return nil, errors.E(op, "invalid type for jwks") } - ks, err := jwk.ParseString(string(b)) + ks, err := jwk.ParseString(jsonString) if err != nil { return nil, errors.E(op, err) } @@ -238,18 +246,18 @@ func (s *VaultStore) GetJWKSPrivateKey(ctx context.Context) ([]byte, error) { return nil, errors.E(op, err) } - secret, err := client.Logical().ReadWithContext(ctx, s.getJWKSPrivateKeyPath()) - if err != nil { + secret, err := client.KVv2(s.KVPath).Get(ctx, s.getJWKSPrivateKeyPath()) + if err != nil && goerr.Unwrap(err) != api.ErrSecretNotFound { return nil, errors.E(op, err) } - if secret == nil { + if secret == nil || secret.Data == nil { msg := "no JWKS private key exists yet." zapctx.Debug(ctx, msg) return nil, errors.E(op, errors.CodeNotFound, msg) } - keyPemB64 := secret.Data["key"].(string) + keyPemB64 := secret.Data[jwksPrivateKey].(string) keyPem, err := base64.StdEncoding.DecodeString(keyPemB64) if err != nil { @@ -268,18 +276,18 @@ func (s *VaultStore) GetJWKSExpiry(ctx context.Context) (time.Time, error) { return now, errors.E(op, err) } - secret, err := client.Logical().ReadWithContext(ctx, s.getJWKSExpiryPath()) - if err != nil { + secret, err := client.KVv2(s.KVPath).Get(ctx, s.getJWKSExpiryPath()) + if err != nil && goerr.Unwrap(err) != api.ErrSecretNotFound { return now, errors.E(op, err) } - if secret == nil { + if secret == nil || secret.Data == nil { msg := "no JWKS expiry exists yet." zapctx.Debug(ctx, msg) return now, errors.E(op, errors.CodeNotFound, msg) } - expiry, ok := secret.Data["jwks-expiry"].(string) + expiry, ok := secret.Data[jwksExpiryKey].(string) if !ok { return now, errors.E(op, "failed to retrieve expiry") } @@ -309,14 +317,8 @@ func (s *VaultStore) PutJWKS(ctx context.Context, jwks jwk.Set) error { return errors.E(op, err) } - _, err = client.Logical().WriteBytesWithContext( - ctx, - // We persist in a similar folder to the controller credentials, but sub-route - // to .well-known for further extensions and mental clarity within our vault. - s.getJWKSPath(), - jwksJson, - ) - if err != nil { + jwksData := map[string]any{jwksKey: string(jwksJson)} + if _, err = client.KVv2(s.KVPath).Put(ctx, s.getJWKSPath(), jwksData); err != nil { return errors.E(op, err) } @@ -332,13 +334,8 @@ func (s *VaultStore) PutJWKSPrivateKey(ctx context.Context, pem []byte) error { return errors.E(op, err) } - if _, err := client.Logical().WriteWithContext( - ctx, - // We persist in a similar folder to the controller credentials, but sub-route - // to .well-known for further extensions and mental clarity within our vault. - s.getJWKSPrivateKeyPath(), - map[string]interface{}{"key": pem}, - ); err != nil { + privateKeyData := map[string]interface{}{jwksPrivateKey: pem} + if _, err := client.KVv2(s.KVPath).Put(ctx, s.getJWKSPrivateKeyPath(), privateKeyData); err != nil { return errors.E(op, err) } return nil @@ -352,14 +349,8 @@ func (s *VaultStore) PutJWKSExpiry(ctx context.Context, expiry time.Time) error if err != nil { return errors.E(op, err) } - - if _, err := client.Logical().WriteWithContext( - ctx, - s.getJWKSExpiryPath(), - map[string]interface{}{ - "jwks-expiry": expiry, - }, - ); err != nil { + expiryData := map[string]interface{}{jwksExpiryKey: expiry} + if _, err := client.KVv2(s.KVPath).Put(ctx, s.getJWKSExpiryPath(), expiryData); err != nil { return errors.E(op, err) } return nil @@ -367,7 +358,7 @@ func (s *VaultStore) PutJWKSExpiry(ctx context.Context, expiry time.Time) error // getWellKnownPath returns a hard coded path to the .well-known credentials. func (s *VaultStore) getWellKnownPath() string { - return path.Join(s.KVPath, "creds", ".well-known") + return path.Join("creds", ".well-known") } // getJWKSPath returns a hardcoded suffixed vault path (dependent on @@ -398,7 +389,7 @@ func (s *VaultStore) CleanupOAuthSecrets(ctx context.Context) error { // Vault does not return errors on deletion requests where // the secret does not exist. - if _, err := client.Logical().DeleteWithContext(ctx, s.GetOAuthSecretPath()); err != nil { + if err := client.KVv2(s.KVPath).Delete(ctx, s.GetOAuthSecretPath()); err != nil { return errors.E(op, err) } return nil @@ -413,25 +404,29 @@ func (s *VaultStore) GetOAuthSecret(ctx context.Context) ([]byte, error) { return nil, errors.E(op, err) } - secret, err := client.Logical().ReadWithContext(ctx, s.GetOAuthSecretPath()) - if err != nil { + secret, err := client.KVv2(s.KVPath).Get(ctx, s.GetOAuthSecretPath()) + if err != nil && goerr.Unwrap(err) != api.ErrSecretNotFound { return nil, errors.E(op, err) } - if secret == nil { + if secret == nil || secret.Data == nil { msg := "no OAuth key exists" zapctx.Debug(ctx, msg) return nil, errors.E(op, errors.CodeNotFound, msg) } - raw := secret.Data["key"] - if secret.Data["key"] == nil { + raw, ok := secret.Data[oAuthSecretKey] + if !ok { msg := "nil OAuth key data" zapctx.Debug(ctx, msg) return nil, errors.E(op, errors.CodeNotFound, msg) } - keyPemB64 := raw.(string) + keyPemB64, ok := raw.(string) + if !ok { + zapctx.Debug(ctx, "oauth secret is not a string") + return nil, errors.E(op, errors.CodeNotFound, "oauth secret not found") + } keyPem, err := base64.StdEncoding.DecodeString(keyPemB64) if err != nil { @@ -450,11 +445,8 @@ func (s *VaultStore) PutOAuthSecret(ctx context.Context, raw []byte) error { return errors.E(op, err) } - if _, err := client.Logical().WriteWithContext( - ctx, - s.GetOAuthSecretPath(), - map[string]interface{}{"key": raw}, - ); err != nil { + oAuthSecretData := map[string]interface{}{oAuthSecretKey: raw} + if _, err := client.KVv2(s.KVPath).Put(ctx, s.GetOAuthSecretPath(), oAuthSecretData); err != nil { return errors.E(op, err) } return nil @@ -463,7 +455,7 @@ func (s *VaultStore) PutOAuthSecret(ctx context.Context, raw []byte) error { // GetOAuthSecretPath returns a hardcoded suffixed vault path (dependent on // the initial KVPath) to the OAuth JWK location. func (s *VaultStore) GetOAuthSecretPath() string { - return path.Join(s.KVPath, "creds", "oauth", "key") + return path.Join("creds", "oauth", "key") } // deleteControllerCredentials removes the credentials associated with the controller in @@ -475,7 +467,7 @@ func (s *VaultStore) deleteControllerCredentials(ctx context.Context, controller if err != nil { return errors.E(op, err) } - _, err = client.Logical().DeleteWithContext(ctx, s.controllerCredentialsPath(controllerName)) + err = client.KVv2(s.KVPath).Delete(ctx, s.controllerCredentialsPath(controllerName)) if rerr, ok := err.(*api.ResponseError); ok && rerr.StatusCode == http.StatusNotFound { // Ignore the error if attempting to delete something that isn't there. err = nil @@ -535,9 +527,9 @@ func (s *VaultStore) client(ctx context.Context) (*api.Client, error) { } func (s *VaultStore) path(tag names.CloudCredentialTag) string { - return path.Join(s.KVPath, "creds", tag.Cloud().Id(), tag.Owner().Id(), tag.Name()) + return path.Join("creds", tag.Cloud().Id(), tag.Owner().Id(), tag.Name()) } func (s *VaultStore) controllerCredentialsPath(controllerName string) string { - return path.Join(s.KVPath, "creds", controllerName) + return path.Join("creds", controllerName) } diff --git a/internal/vault/vault_test.go b/internal/vault/vault_test.go index 3d6032e2a..2594258e8 100644 --- a/internal/vault/vault_test.go +++ b/internal/vault/vault_test.go @@ -209,7 +209,7 @@ func TestGetOAuthSecretFailsIfDataIsNil(t *testing.T) { c.Assert(err, qt.IsNil) retrieved, err := store.GetOAuthSecret(ctx) - c.Assert(err, qt.ErrorMatches, "nil OAuth key data") + c.Assert(err, qt.ErrorMatches, "oauth secret not found") c.Assert(retrieved, qt.IsNil) } diff --git a/internal/wellknownapi/api_test.go b/internal/wellknownapi/api_test.go index 8f809d5b2..923a26e0b 100644 --- a/internal/wellknownapi/api_test.go +++ b/internal/wellknownapi/api_test.go @@ -60,7 +60,7 @@ func setupHandlerAndRecorder(c *qt.C, path string, store *vault.VaultStore) *htt return rr } -// 404: In the event the JWKS cannot be found expliciticly from +// 404: In the event the JWKS cannot be found explicitly from // the credential store. func TestWellknownAPIJWKSJSONHandles404(t *testing.T) { c := qt.New(t) diff --git a/local/vault/init.sh b/local/vault/init.sh index 2ff44ad22..986e5d6d2 100755 --- a/local/vault/init.sh +++ b/local/vault/init.sh @@ -46,7 +46,7 @@ echo "SecretID applied & wrapped in cubbyhole for 10h, token is: $JIMM_SECRET_WR # Enable the KV at the defined policy path echo "Enabling KV at policy path /jimm-kv" echo "/jimm-kv accessible by policy jimm-app" -vault secrets enable -path /jimm-kv kv +vault secrets enable -version=2 -path /jimm-kv kv echo "Creating approle auth file." VAULT_TOKEN=$JIMM_SECRET_WRAPPED vault unwrap > /vault/approle_tmp.yaml echo "$JIMM_ROLE_ID" > /vault/roleid.txt diff --git a/service.go b/service.go index c0d8eec72..62d9c15fd 100644 --- a/service.go +++ b/service.go @@ -488,7 +488,7 @@ func newVaultStore(ctx context.Context, p Params) (jimmcreds.CredentialStore, er Client: client, RoleID: p.VaultRoleID, RoleSecretID: p.VaultRoleSecretID, - KVPath: p.VaultPath, + KVPath: strings.ReplaceAll(p.VaultPath, "/", ""), }, nil } From d7037a8c96ee700fd9101c346d3058f0c1258296 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 17 May 2024 11:56:27 +0200 Subject: [PATCH 125/126] Include auth model in OCI image (#1212) * Include auth model in OCI image * Make OpenFGA top levels and add symlink for test folder * Remove mkdir line --------- Co-authored-by: Ales Stimec --- Dockerfile | 1 + local/openfga/authorisation_model.json | 449 +----------------- .../authorisation_model.fga | 0 openfga/authorisation_model.json | 448 +++++++++++++++++ 4 files changed, 450 insertions(+), 448 deletions(-) mode change 100644 => 120000 local/openfga/authorisation_model.json rename {local/openfga => openfga}/authorisation_model.fga (100%) create mode 100644 openfga/authorisation_model.json diff --git a/Dockerfile b/Dockerfile index 99b71535f..787ce5bac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ LABEL org.opencontainers.image.source=https://github.com/canonical/jimm LABEL org.opencontainers.image.description="JIMM server container image" RUN apt-get -qq update && apt-get -qq install -y ca-certificates postgresql-client WORKDIR /root/ +COPY --from=build-env /usr/src/jimm/openfga/authorisation_model.json ./openfga/ COPY --from=build-env /usr/src/jimm/jimmsrv . COPY --from=build-env /usr/src/jimm/internal/dbmodel/sql ./sql/ ENTRYPOINT [ "./jimmsrv" ] diff --git a/local/openfga/authorisation_model.json b/local/openfga/authorisation_model.json deleted file mode 100644 index 7787b5ff0..000000000 --- a/local/openfga/authorisation_model.json +++ /dev/null @@ -1,448 +0,0 @@ -{ - "schema_version": "1.1", - "type_definitions": [ - { - "metadata": { - "relations": { - "administrator": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "consumer": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "model": { - "directly_related_user_types": [ - { - "type": "model" - } - ] - }, - "reader": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - } - } - }, - "relations": { - "administrator": { - "union": { - "child": [ - { - "this": {} - }, - { - "tupleToUserset": { - "computedUserset": { - "relation": "administrator" - }, - "tupleset": { - "relation": "model" - } - } - } - ] - } - }, - "consumer": { - "union": { - "child": [ - { - "this": {} - }, - { - "computedUserset": { - "relation": "administrator" - } - } - ] - } - }, - "model": { - "this": {} - }, - "reader": { - "union": { - "child": [ - { - "this": {} - }, - { - "computedUserset": { - "relation": "consumer" - } - } - ] - } - } - }, - "type": "applicationoffer" - }, - { - "metadata": { - "relations": { - "administrator": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "can_addmodel": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "controller": { - "directly_related_user_types": [ - { - "type": "controller" - } - ] - } - } - }, - "relations": { - "administrator": { - "union": { - "child": [ - { - "this": {} - }, - { - "tupleToUserset": { - "computedUserset": { - "relation": "administrator" - }, - "tupleset": { - "relation": "controller" - } - } - } - ] - } - }, - "can_addmodel": { - "union": { - "child": [ - { - "this": {} - }, - { - "computedUserset": { - "relation": "administrator" - } - } - ] - } - }, - "controller": { - "this": {} - } - }, - "type": "cloud" - }, - { - "metadata": { - "relations": { - "administrator": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "audit_log_viewer": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "controller": { - "directly_related_user_types": [ - { - "type": "controller" - } - ] - } - } - }, - "relations": { - "administrator": { - "union": { - "child": [ - { - "this": {} - }, - { - "tupleToUserset": { - "computedUserset": { - "relation": "administrator" - }, - "tupleset": { - "relation": "controller" - } - } - } - ] - } - }, - "audit_log_viewer": { - "union": { - "child": [ - { - "this": {} - }, - { - "computedUserset": { - "relation": "administrator" - } - } - ] - } - }, - "controller": { - "this": {} - } - }, - "type": "controller" - }, - { - "metadata": { - "relations": { - "member": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - } - } - }, - "relations": { - "member": { - "this": {} - } - }, - "type": "group" - }, - { - "metadata": { - "relations": { - "administrator": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "controller": { - "directly_related_user_types": [ - { - "type": "controller" - } - ] - }, - "reader": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "writer": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - } - } - }, - "relations": { - "administrator": { - "union": { - "child": [ - { - "this": {} - }, - { - "tupleToUserset": { - "computedUserset": { - "relation": "administrator" - }, - "tupleset": { - "relation": "controller" - } - } - } - ] - } - }, - "controller": { - "this": {} - }, - "reader": { - "union": { - "child": [ - { - "this": {} - }, - { - "computedUserset": { - "relation": "writer" - } - } - ] - } - }, - "writer": { - "union": { - "child": [ - { - "this": {} - }, - { - "computedUserset": { - "relation": "administrator" - } - } - ] - } - } - }, - "type": "model" - }, - { - "type": "user" - }, - { - "metadata": { - "relations": { - "administrator": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - } - } - }, - "relations": { - "administrator": { - "this": {} - } - }, - "type": "serviceaccount" - } - ] -} diff --git a/local/openfga/authorisation_model.json b/local/openfga/authorisation_model.json new file mode 120000 index 000000000..97998ba1b --- /dev/null +++ b/local/openfga/authorisation_model.json @@ -0,0 +1 @@ +../../openfga/authorisation_model.json \ No newline at end of file diff --git a/local/openfga/authorisation_model.fga b/openfga/authorisation_model.fga similarity index 100% rename from local/openfga/authorisation_model.fga rename to openfga/authorisation_model.fga diff --git a/openfga/authorisation_model.json b/openfga/authorisation_model.json new file mode 100644 index 000000000..7787b5ff0 --- /dev/null +++ b/openfga/authorisation_model.json @@ -0,0 +1,448 @@ +{ + "schema_version": "1.1", + "type_definitions": [ + { + "metadata": { + "relations": { + "administrator": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "relation": "member", + "type": "group" + } + ] + }, + "consumer": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "relation": "member", + "type": "group" + } + ] + }, + "model": { + "directly_related_user_types": [ + { + "type": "model" + } + ] + }, + "reader": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "relation": "member", + "type": "group" + } + ] + } + } + }, + "relations": { + "administrator": { + "union": { + "child": [ + { + "this": {} + }, + { + "tupleToUserset": { + "computedUserset": { + "relation": "administrator" + }, + "tupleset": { + "relation": "model" + } + } + } + ] + } + }, + "consumer": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "relation": "administrator" + } + } + ] + } + }, + "model": { + "this": {} + }, + "reader": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "relation": "consumer" + } + } + ] + } + } + }, + "type": "applicationoffer" + }, + { + "metadata": { + "relations": { + "administrator": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "relation": "member", + "type": "group" + } + ] + }, + "can_addmodel": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "relation": "member", + "type": "group" + } + ] + }, + "controller": { + "directly_related_user_types": [ + { + "type": "controller" + } + ] + } + } + }, + "relations": { + "administrator": { + "union": { + "child": [ + { + "this": {} + }, + { + "tupleToUserset": { + "computedUserset": { + "relation": "administrator" + }, + "tupleset": { + "relation": "controller" + } + } + } + ] + } + }, + "can_addmodel": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "relation": "administrator" + } + } + ] + } + }, + "controller": { + "this": {} + } + }, + "type": "cloud" + }, + { + "metadata": { + "relations": { + "administrator": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "relation": "member", + "type": "group" + } + ] + }, + "audit_log_viewer": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "relation": "member", + "type": "group" + } + ] + }, + "controller": { + "directly_related_user_types": [ + { + "type": "controller" + } + ] + } + } + }, + "relations": { + "administrator": { + "union": { + "child": [ + { + "this": {} + }, + { + "tupleToUserset": { + "computedUserset": { + "relation": "administrator" + }, + "tupleset": { + "relation": "controller" + } + } + } + ] + } + }, + "audit_log_viewer": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "relation": "administrator" + } + } + ] + } + }, + "controller": { + "this": {} + } + }, + "type": "controller" + }, + { + "metadata": { + "relations": { + "member": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "relation": "member", + "type": "group" + } + ] + } + } + }, + "relations": { + "member": { + "this": {} + } + }, + "type": "group" + }, + { + "metadata": { + "relations": { + "administrator": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "relation": "member", + "type": "group" + } + ] + }, + "controller": { + "directly_related_user_types": [ + { + "type": "controller" + } + ] + }, + "reader": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "relation": "member", + "type": "group" + } + ] + }, + "writer": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "relation": "member", + "type": "group" + } + ] + } + } + }, + "relations": { + "administrator": { + "union": { + "child": [ + { + "this": {} + }, + { + "tupleToUserset": { + "computedUserset": { + "relation": "administrator" + }, + "tupleset": { + "relation": "controller" + } + } + } + ] + } + }, + "controller": { + "this": {} + }, + "reader": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "relation": "writer" + } + } + ] + } + }, + "writer": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "relation": "administrator" + } + } + ] + } + } + }, + "type": "model" + }, + { + "type": "user" + }, + { + "metadata": { + "relations": { + "administrator": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "relation": "member", + "type": "group" + } + ] + } + } + }, + "relations": { + "administrator": { + "this": {} + } + }, + "type": "serviceaccount" + } + ] +} From 5541d6af7d1af0c43562d2c56f6e9634ae54f554 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Tue, 21 May 2024 08:16:18 +0100 Subject: [PATCH 126/126] Upgrade to juju 3.5 (#1206) * Upgrade to juju 3.5 * Test fix --------- Co-authored-by: Ales Stimec --- go.mod | 17 +- go.sum | 31 +- internal/db/model.go | 2 +- internal/dbmodel/applicationoffer.go | 166 ++++---- internal/jimm/applicationoffer.go | 28 +- internal/jimm/applicationoffer_test.go | 359 ++---------------- internal/jimm/jimm.go | 6 +- internal/jimmtest/api.go | 12 +- internal/jimmtest/jimm_mock.go | 12 +- internal/jujuapi/applicationoffers.go | 10 +- internal/jujuapi/applicationoffers_test.go | 4 +- internal/jujuapi/controllerroot.go | 6 +- internal/jujuapi/modelmanager_test.go | 2 +- internal/jujuclient/applicationoffers.go | 22 +- internal/jujuclient/applicationoffers_test.go | 38 +- 15 files changed, 230 insertions(+), 485 deletions(-) diff --git a/go.mod b/go.mod index ade08dba0..a4c537469 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/juju/cmd/v3 v3.0.14 github.com/juju/errors v1.0.0 github.com/juju/gnuflag v1.0.0 - github.com/juju/juju v0.0.0-20240304110523-55fb5d03683b + github.com/juju/juju v0.0.0-20240423234833-93553287462a github.com/juju/loggo v1.0.0 github.com/juju/mgomonitor v0.0.0-20181029151116-52206bb0cd31 github.com/juju/names/v4 v4.0.0 @@ -104,7 +104,10 @@ require ( github.com/aws/smithy-go v1.19.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f // indirect + github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8 // indirect github.com/canonical/lxd v0.0.0-20231214113525-e676fc63c50a // indirect + github.com/canonical/pebble v1.10.2 // indirect + github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b // indirect github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cjlapao/common-go v0.0.39 // indirect @@ -144,6 +147,7 @@ require ( github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/schema v1.2.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -173,7 +177,7 @@ require ( github.com/juju/blobstore/v3 v3.0.2 // indirect github.com/juju/clock v1.0.3 // indirect github.com/juju/collections v1.0.4 // indirect - github.com/juju/description/v5 v5.0.0 // indirect + github.com/juju/description/v5 v5.0.4 // indirect github.com/juju/featureflag v1.0.0 // indirect github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a // indirect github.com/juju/gojsonpointer v0.0.0-20150204194629-afe8b77aa08f // indirect @@ -190,7 +194,7 @@ require ( github.com/juju/mutex/v2 v2.0.0 // indirect github.com/juju/naturalsort v1.0.0 // indirect github.com/juju/os/v2 v2.2.3 // indirect - github.com/juju/packaging/v2 v2.0.1 // indirect + github.com/juju/packaging/v3 v3.0.0 // indirect github.com/juju/persistent-cookiejar v1.0.0 // indirect github.com/juju/proxy v1.0.0 // indirect github.com/juju/pubsub/v2 v2.0.0 // indirect @@ -202,7 +206,7 @@ require ( github.com/juju/schema v1.2.0 // indirect github.com/juju/txn/v3 v3.0.2 // indirect github.com/juju/usso v1.0.1 // indirect - github.com/juju/utils/v3 v3.1.0 // indirect + github.com/juju/utils/v3 v3.1.1 // indirect github.com/juju/viddy v0.0.0-beta5 // indirect github.com/juju/webbrowser v1.0.0 // indirect github.com/juju/worker/v3 v3.5.0 // indirect @@ -260,6 +264,7 @@ require ( github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/sftp v1.13.6 // indirect + github.com/pkg/term v1.1.0 // indirect github.com/pkg/xattr v0.4.9 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.5.0 // indirect @@ -299,15 +304,17 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.22.0 // indirect golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/mod v0.14.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.16.1 // indirect google.golang.org/api v0.154.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect google.golang.org/grpc v1.59.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/gobwas/glob.v0 v0.2.3 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 375f90f37..878eda19b 100644 --- a/go.sum +++ b/go.sum @@ -145,12 +145,18 @@ github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f h1:gOO/tNZMjjvTKZWpY github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= github.com/canonical/go-dqlite v1.21.0 h1:4gLDdV2GF+vg0yv9Ff+mfZZNQ1JGhnQ3GnS2GeZPHfA= github.com/canonical/go-dqlite v1.21.0/go.mod h1:Uvy943N8R4CFUAs59A1NVaziWY9nJ686lScY7ywurfg= +github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8 h1:zGaJEJI9qPVyM+QKFJagiyrM91Ke5S9htoL1D470g6E= +github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8/go.mod h1:ZZFeR9K9iGgpwOaLYF9PdT44/+lfSJ9sQz3B+SsGsYU= github.com/canonical/go-service v1.0.0 h1:TF6TsEp04xAoI5pPoWjTYmEwLjbPATSnHEyeJCvzElg= github.com/canonical/go-service v1.0.0/go.mod h1:GzNLXpkGdglL0kjREXoLXj2rB2Qx+EvAGncRDqCENYQ= github.com/canonical/lxd v0.0.0-20231214113525-e676fc63c50a h1:Tfo/MzXK5GeG7gzSHqxGeY/669Mhh5ea43dn1mRDnk8= github.com/canonical/lxd v0.0.0-20231214113525-e676fc63c50a/go.mod h1:UxfHGKFoRjgu1NUA9EFiR++dKvyAiT0h9HT0ffMlzjc= github.com/canonical/ofga v0.10.0 h1:DHXhG/DAXWWQT/I+2jzr4qm0uTIYrILmtMxd6ZqmEzE= github.com/canonical/ofga v0.10.0/go.mod h1:u4Ou8dbIhO7FmVlT7W3rX2roD9AOGz/CqmGh7AdF0Lo= +github.com/canonical/pebble v1.10.2 h1:TG0RYLqH+WEjnxsTB1JbaW0wzeygG0/dPHEEFQKn2JE= +github.com/canonical/pebble v1.10.2/go.mod h1:BXpt85cFqrBgACeVRrTQ7JxZIdnGILv32V7mAfDcGFc= +github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b h1:Da2fardddn+JDlVEYtrzBLTtyzoyU3nIS0Cf0GvjmwU= +github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b/go.mod h1:upTK9n6rlqITN9rCN69hdreI37dRDFUk2thlGGD5Cg8= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= @@ -588,8 +594,8 @@ github.com/juju/collections v0.0.0-20220203020748-febd7cad8a7a/go.mod h1:JWeZdyt github.com/juju/collections v1.0.0/go.mod h1:JWeZdyttIEbkR51z2S13+J+aCuHVe0F6meRy+P0YGDo= github.com/juju/collections v1.0.4 h1:GjL+aN512m2rVDqhPII7P6qB0e+iYFubz8sqBhZaZtk= github.com/juju/collections v1.0.4/go.mod h1:hVrdB0Zwq9wIU1Fl6ItD2+UETeNeOEs+nGvJufVe+0c= -github.com/juju/description/v5 v5.0.0 h1:koySpaGHVTvoHlr+siRLxVKS/Jzilud5iGzjE7tldks= -github.com/juju/description/v5 v5.0.0/go.mod h1:TQsp2Z56EVab7onQY2/O6tLHznovAcckyLz/DgDfVPY= +github.com/juju/description/v5 v5.0.4 h1:qA35hRglZ47j1mmo9zUM9R+2WSDCH5dvL5ik7gA2aVE= +github.com/juju/description/v5 v5.0.4/go.mod h1:TQsp2Z56EVab7onQY2/O6tLHznovAcckyLz/DgDfVPY= github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/errors v0.0.0-20200330140219-3fe23663418f/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/errors v0.0.0-20210818161939-5560c4c073ff/go.mod h1:i1eL7XREII6aHpQ2gApI/v6FkVUDEBremNkcBCKYAcY= @@ -618,8 +624,8 @@ github.com/juju/idmclient/v2 v2.0.0 h1:PsGa092JGy6iFNHZCcao+bigVsTyz1C+tHNRdYmKv github.com/juju/idmclient/v2 v2.0.0/go.mod h1:EOiFbPmnkqKvCUS/DHpDRWhL7eKF0AJaTvMjIYlIUak= github.com/juju/jsonschema v1.0.0 h1:2ScR9hhVdHxft+Te3fnclVx61MmlikHNEOirTGi+hV4= github.com/juju/jsonschema v1.0.0/go.mod h1:SlFW+jFtpWX0P4Tb+zTTPR4ufttLrnJIdQPePxVEfkM= -github.com/juju/juju v0.0.0-20240304110523-55fb5d03683b h1:rowVYnJXJQup9dFBNLx3PCa9NWZ63jgewlwHB2HDBEk= -github.com/juju/juju v0.0.0-20240304110523-55fb5d03683b/go.mod h1:lG1192+QZsfFbVI+3Vg9KDv+F2tGc+8marXD9E1Vnuc= +github.com/juju/juju v0.0.0-20240423234833-93553287462a h1:ElPIIOVZ50QJjwXukV1mRRdW9kGXxvK8MZz6iP72su8= +github.com/juju/juju v0.0.0-20240423234833-93553287462a/go.mod h1:LN4FXgbHGi5VsBjy0gs+xXDWM0eiOHaAyl6QDF7bCjY= github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20190212223446-d976af380377/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20200526014432-9ce3a2e09b5e/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= @@ -652,8 +658,8 @@ github.com/juju/naturalsort v1.0.0 h1:kGmUUy3h8mJ5/SJYaqKOBR3f3owEd5R52Lh+Tjg/dN github.com/juju/naturalsort v1.0.0/go.mod h1:Zqa/vGkXr78k47zM6tFmU9phhxKz/PIdqBzpLhJ86zc= github.com/juju/os/v2 v2.2.3 h1:5SnGWfzFTXcFwu/sd9qEEf/No3UZxivOjJuWmsHI4N4= github.com/juju/os/v2 v2.2.3/go.mod h1:xGfP9I+Xb/A03NcGBsoJgwr084hPckkQHecaHuV3wBQ= -github.com/juju/packaging/v2 v2.0.1 h1:KeTfqx3Z0c6RcM053GJH7mplroXoRSuh/dK5vqDQLn8= -github.com/juju/packaging/v2 v2.0.1/go.mod h1:JC+FIRTJXGLt9wA+iP3ltkzv+aWVMMojB/R47uIAK0Y= +github.com/juju/packaging/v3 v3.0.0 h1:ZzuHhR8Ql9z2oeQ0m73x6k58PW65Cgk5wR9Yc1exoOI= +github.com/juju/packaging/v3 v3.0.0/go.mod h1:WXh/SXqh1du8SFzwb1KC+yZuV4Qc4alWP3MEPqFX9Lw= github.com/juju/persistent-cookiejar v1.0.0 h1:Ag7+QLzqC2m+OYXy2QQnRjb3gTkEBSZagZ6QozwT3EQ= github.com/juju/persistent-cookiejar v1.0.0/go.mod h1:zrbmo4nBKaiP/Ez3F67ewkMbzGYfXyMvRtbOfuAwG0w= github.com/juju/postgrestest v1.1.0/go.mod h1:/n17Y2T6iFozzXwSCO0JYJ5gSiz2caEtSwAjh/uLXDM= @@ -705,8 +711,8 @@ github.com/juju/utils/v3 v3.0.0-20220130232349-cd7ecef0e94a/go.mod h1:LzwbbEN7bu github.com/juju/utils/v3 v3.0.0-20220202114721-338bb0530e89/go.mod h1:wf5w+8jyTh2IYnSX0sHnMJo4ZPwwuiBWn+xN3DkQg4k= github.com/juju/utils/v3 v3.0.0-20220203023959-c3fbc78a33b0/go.mod h1:8csUcj1VRkfjNIRzBFWzLFCMLwLqsRWvkmhfVAUwbC4= github.com/juju/utils/v3 v3.0.0/go.mod h1:8csUcj1VRkfjNIRzBFWzLFCMLwLqsRWvkmhfVAUwbC4= -github.com/juju/utils/v3 v3.1.0 h1:NrNo73oVtfr7kLP17/BDpubXwa7YEW16+Ult6z9kpHI= -github.com/juju/utils/v3 v3.1.0/go.mod h1:nAj3sHtdYfAkvnkqttTy3Xzm2HzkD9Hfgnc+upOW2Z8= +github.com/juju/utils/v3 v3.1.1 h1:shEMr/4Wkw0YCOPz5IFOYkLv1ec50pzRi59TRl0qQ/0= +github.com/juju/utils/v3 v3.1.1/go.mod h1:nAj3sHtdYfAkvnkqttTy3Xzm2HzkD9Hfgnc+upOW2Z8= github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= github.com/juju/version v0.0.0-20191219164919-81c1be00b9a6/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= @@ -928,6 +934,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= +github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= +github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -1178,6 +1186,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1300,6 +1310,7 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1552,8 +1563,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/db/model.go b/internal/db/model.go index c4e1c6448..68dcd5bab 100644 --- a/internal/db/model.go +++ b/internal/db/model.go @@ -177,7 +177,7 @@ func preloadModel(prefix string, db *gorm.DB) *gorm.DB { // loading a model, as we just use the credential to deploy // applications. db = db.Preload(prefix + "CloudCredential") - db = db.Preload(prefix + "Offers").Preload(prefix + "Offers.Connections").Preload(prefix + "Offers.Endpoints").Preload(prefix + "Offers.Spaces") + db = db.Preload(prefix + "Offers").Preload(prefix + "Offers.Connections").Preload(prefix + "Offers.Endpoints") return db } diff --git a/internal/dbmodel/applicationoffer.go b/internal/dbmodel/applicationoffer.go index 60f29dcdf..555e71c70 100644 --- a/internal/dbmodel/applicationoffer.go +++ b/internal/dbmodel/applicationoffer.go @@ -69,96 +69,39 @@ func (o *ApplicationOffer) SetTag(t names.ApplicationOfferTag) { o.UUID = t.Id() } -// FromJujuApplicationOfferAdminDetails fills in the information from jujuparams ApplicationOfferAdminDetails. -func (o *ApplicationOffer) FromJujuApplicationOfferAdminDetails(offerDetails jujuparams.ApplicationOfferAdminDetails) { +// FromJujuApplicationOfferAdminDetails maps the Juju ApplicationOfferDetails struct type to a JIMM type +// such that it can be persisted. +func (o *ApplicationOffer) FromJujuApplicationOfferAdminDetailsV5(offerDetails jujuparams.ApplicationOfferAdminDetailsV5) { o.ApplicationName = offerDetails.ApplicationName o.ApplicationDescription = offerDetails.ApplicationDescription o.Name = offerDetails.OfferName o.UUID = offerDetails.OfferUUID o.URL = offerDetails.OfferURL - o.Bindings = offerDetails.Bindings o.CharmURL = offerDetails.CharmURL - - o.Endpoints = make([]ApplicationOfferRemoteEndpoint, len(offerDetails.Endpoints)) - for i, endpoint := range offerDetails.Endpoints { - o.Endpoints[i] = ApplicationOfferRemoteEndpoint{ - Name: endpoint.Name, - Role: string(endpoint.Role), - Interface: endpoint.Interface, - Limit: endpoint.Limit, - } - } - - o.Spaces = make([]ApplicationOfferRemoteSpace, len(offerDetails.Spaces)) - for i, space := range offerDetails.Spaces { - o.Spaces[i] = ApplicationOfferRemoteSpace{ - CloudType: space.CloudType, - Name: space.Name, - ProviderID: space.ProviderId, - ProviderAttributes: space.ProviderAttributes, - } - } - - o.Connections = make([]ApplicationOfferConnection, len(offerDetails.Connections)) - for i, connection := range offerDetails.Connections { - o.Connections[i] = ApplicationOfferConnection{ - SourceModelTag: connection.SourceModelTag, - RelationID: connection.RelationId, - IdentityName: connection.Username, - Endpoint: connection.Endpoint, - IngressSubnets: connection.IngressSubnets, - } - } + o.Endpoints = mapJujuRemoteEndpointsToJIMMRemoteEndpoints(offerDetails.Endpoints) + o.Connections = mapJujuConnectionsToJIMMConnections(offerDetails.Connections) } -// ToJujuApplicationOfferDetails returns a jujuparams ApplicationOfferAdminDetails based on the application offer. -func (o *ApplicationOffer) ToJujuApplicationOfferDetails() jujuparams.ApplicationOfferAdminDetails { - endpoints := make([]jujuparams.RemoteEndpoint, len(o.Endpoints)) - for i, endpoint := range o.Endpoints { - endpoints[i] = jujuparams.RemoteEndpoint{ - Name: endpoint.Name, - Role: charm.RelationRole(endpoint.Role), - Interface: endpoint.Interface, - Limit: endpoint.Limit, - } - } - spaces := make([]jujuparams.RemoteSpace, len(o.Spaces)) - for i, space := range o.Spaces { - spaces[i] = jujuparams.RemoteSpace{ - CloudType: space.CloudType, - Name: space.Name, - ProviderId: space.ProviderID, - ProviderAttributes: space.ProviderAttributes, - } +// ToJujuApplicationOfferDetails maps the JIMM ApplicationOfferDetails struct type to a jujuparams type +// such that it can be sent over the wire. +func (o *ApplicationOffer) ToJujuApplicationOfferDetailsV5() jujuparams.ApplicationOfferAdminDetailsV5 { + v5Details := jujuparams.ApplicationOfferDetailsV5{ + SourceModelTag: o.Model.Tag().String(), + OfferUUID: o.UUID, + OfferURL: o.URL, + OfferName: o.Name, + ApplicationDescription: o.ApplicationDescription, + Endpoints: mapJIMMRemoteEndpointsToJujuRemoteEndpoints(o.Endpoints), } - connections := make([]jujuparams.OfferConnection, len(o.Connections)) - for i, connection := range o.Connections { - connections[i] = jujuparams.OfferConnection{ - SourceModelTag: connection.SourceModelTag, - RelationId: connection.RelationID, - Username: connection.IdentityName, - Endpoint: connection.Endpoint, - IngressSubnets: connection.IngressSubnets, - } - } - return jujuparams.ApplicationOfferAdminDetails{ - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ - SourceModelTag: o.Model.Tag().String(), - OfferUUID: o.UUID, - OfferURL: o.URL, - OfferName: o.Name, - ApplicationDescription: o.ApplicationDescription, - Endpoints: endpoints, - Spaces: spaces, - Bindings: o.Bindings, - //TODO(Kian) CSS-6040 Refactor the below to use a better abstraction for Postgres/OpenFGA to Juju types - Users: nil, - }, - ApplicationName: o.ApplicationName, - CharmURL: o.CharmURL, - Connections: connections, + v5AdminDetails := jujuparams.ApplicationOfferAdminDetailsV5{ + ApplicationOfferDetailsV5: v5Details, + ApplicationName: o.ApplicationName, + CharmURL: o.CharmURL, + Connections: mapJIMMConnectionsToJujuConnections(o.Connections), } + + return v5AdminDetails } // ApplicationOfferRemoteEndpoint represents a remote application endpoint. @@ -203,3 +146,68 @@ type ApplicationOfferConnection struct { Endpoint string IngressSubnets Strings } + +// mapJIMMRemoteEndpointsToJujuRemoteEndpoints maps the types between JIMM's +// remote endpoints type (with gorm embedded for persistence) to a jujuparams +// remote endpoint, such that it can be sent over the wire and contains the correct +// json tags. +func mapJIMMRemoteEndpointsToJujuRemoteEndpoints(endpoints []ApplicationOfferRemoteEndpoint) []jujuparams.RemoteEndpoint { + mappedEndpoints := make([]jujuparams.RemoteEndpoint, len(endpoints)) + for i, endpoint := range endpoints { + mappedEndpoints[i] = jujuparams.RemoteEndpoint{ + Name: endpoint.Name, + Role: charm.RelationRole(endpoint.Role), + Interface: endpoint.Interface, + Limit: endpoint.Limit, + } + } + return mappedEndpoints +} + +// mapJujuRemoteEndpointsToJIMMRemoteEndpoints - See above for details, this does the opposite. +func mapJujuRemoteEndpointsToJIMMRemoteEndpoints(endpoints []jujuparams.RemoteEndpoint) []ApplicationOfferRemoteEndpoint { + mappedEndpoints := make([]ApplicationOfferRemoteEndpoint, len(endpoints)) + for i, endpoint := range endpoints { + mappedEndpoints[i] = ApplicationOfferRemoteEndpoint{ + Name: endpoint.Name, + Role: string(endpoint.Role), + Interface: endpoint.Interface, + Limit: endpoint.Limit, + } + } + return mappedEndpoints +} + +// mapJIMMConnectionsToJujuConnections maps the types between JIMM's +// offer connection type (with gorm embedded for persistence) to a jujuparams +// offer connection, such that it can be sent over the wire and contains the correct +// json tags. +func mapJIMMConnectionsToJujuConnections(connections []ApplicationOfferConnection) []jujuparams.OfferConnection { + mappedConnections := make([]jujuparams.OfferConnection, len(connections)) + for i, connection := range connections { + mappedConnections[i] = jujuparams.OfferConnection{ + SourceModelTag: connection.SourceModelTag, + RelationId: connection.RelationID, + Username: connection.IdentityName, + Endpoint: connection.Endpoint, + IngressSubnets: connection.IngressSubnets, + // TODO(ale8k): Status is missing, do we need it?? + } + } + return mappedConnections +} + +// mapJujuConnectionsToJIMMConnections - See above for details, this does the opposite. +func mapJujuConnectionsToJIMMConnections(connections []jujuparams.OfferConnection) []ApplicationOfferConnection { + mappedConnections := make([]ApplicationOfferConnection, len(connections)) + for i, connection := range connections { + mappedConnections[i] = ApplicationOfferConnection{ + SourceModelTag: connection.SourceModelTag, + RelationID: connection.RelationId, + IdentityName: connection.Username, + Endpoint: connection.Endpoint, + IngressSubnets: connection.IngressSubnets, + } + } + return mappedConnections +} diff --git a/internal/jimm/applicationoffer.go b/internal/jimm/applicationoffer.go index 2cb7f59b1..d9189b824 100644 --- a/internal/jimm/applicationoffer.go +++ b/internal/jimm/applicationoffer.go @@ -95,8 +95,8 @@ func (j *JIMM) Offer(ctx context.Context, user *openfga.User, offer AddApplicati return errors.E(op, err) } - offerDetails := jujuparams.ApplicationOfferAdminDetails{ - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ + offerDetails := jujuparams.ApplicationOfferAdminDetailsV5{ + ApplicationOfferDetailsV5: jujuparams.ApplicationOfferDetailsV5{ OfferURL: offerURL.String(), }, } @@ -107,7 +107,7 @@ func (j *JIMM) Offer(ctx context.Context, user *openfga.User, offer AddApplicati } var doc dbmodel.ApplicationOffer - doc.FromJujuApplicationOfferAdminDetails(offerDetails) + doc.FromJujuApplicationOfferAdminDetailsV5(offerDetails) if err != nil { zapctx.Error(ctx, "failed to convert application offer details", zaputil.Error(err)) return errors.E(op, err) @@ -299,7 +299,7 @@ func (j *JIMM) listApplicationOfferUsers(ctx context.Context, offer names.Applic } // GetApplicationOffer returns details of the offer with the specified URL. -func (j *JIMM) GetApplicationOffer(ctx context.Context, user *openfga.User, offerURL string) (*jujuparams.ApplicationOfferAdminDetails, error) { +func (j *JIMM) GetApplicationOffer(ctx context.Context, user *openfga.User, offerURL string) (*jujuparams.ApplicationOfferAdminDetailsV5, error) { const op = errors.Op("jimm.GetApplicationOffer") offer := dbmodel.ApplicationOffer{ @@ -342,7 +342,7 @@ func (j *JIMM) GetApplicationOffer(ctx context.Context, user *openfga.User, offe } defer api.Close() - var offerDetails jujuparams.ApplicationOfferAdminDetails + var offerDetails jujuparams.ApplicationOfferAdminDetailsV5 offerDetails.OfferURL = offerURL if err := api.GetApplicationOffer(ctx, &offerDetails); err != nil { return nil, errors.E(op, err) @@ -546,8 +546,8 @@ func (j *JIMM) UpdateApplicationOffer(ctx context.Context, controller *dbmodel.C } defer api.Close() - offerDetails := jujuparams.ApplicationOfferAdminDetails{ - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ + offerDetails := jujuparams.ApplicationOfferAdminDetailsV5{ + ApplicationOfferDetailsV5: jujuparams.ApplicationOfferDetailsV5{ OfferURL: offer.URL, }, } @@ -556,7 +556,7 @@ func (j *JIMM) UpdateApplicationOffer(ctx context.Context, controller *dbmodel.C return errors.E(op, err) } - offer.FromJujuApplicationOfferAdminDetails(offerDetails) + offer.FromJujuApplicationOfferAdminDetailsV5(offerDetails) err = j.Database.UpdateApplicationOffer(ctx, &offer) if err != nil { return errors.E(op, err) @@ -595,7 +595,7 @@ func (j *JIMM) getUserOfferAccess(ctx context.Context, user *openfga.User, offer } // FindApplicationOffers returns details of offers matching the specified filter. -func (j *JIMM) FindApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) { +func (j *JIMM) FindApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) { const op = errors.Op("jimm.FindApplicationOffers") if len(filters) == 0 { @@ -615,7 +615,7 @@ func (j *JIMM) FindApplicationOffers(ctx context.Context, user *openfga.User, fi if err != nil { return nil, errors.E(op, err) } - offerDetails := make([]jujuparams.ApplicationOfferAdminDetails, len(offers)) + offerDetails := make([]jujuparams.ApplicationOfferAdminDetailsV5, len(offers)) for i, offer := range offers { // TODO (alesstimec) Optimize this: currently check all possible // permission levels for an offer, this is suboptimal. @@ -624,7 +624,7 @@ func (j *JIMM) FindApplicationOffers(ctx context.Context, user *openfga.User, fi return nil, errors.E(op, err) } - offerDetails[i] = offer.ToJujuApplicationOfferDetails() + offerDetails[i] = offer.ToJujuApplicationOfferDetailsV5() // non-admin users should not see connections of an application // offer. @@ -703,7 +703,7 @@ func (j *JIMM) applicationOfferFilters(ctx context.Context, jujuFilters ...jujup } // ListApplicationOffers returns details of offers matching the specified filter. -func (j *JIMM) ListApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) { +func (j *JIMM) ListApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) { const op = errors.Op("jimm.ListApplicationOffers") type modelKey struct { @@ -730,7 +730,7 @@ func (j *JIMM) ListApplicationOffers(ctx context.Context, user *openfga.User, fi modelFilters[m] = append(modelFilters[m], f) } - var offers []jujuparams.ApplicationOfferAdminDetails + var offers []jujuparams.ApplicationOfferAdminDetailsV5 var keys []modelKey for k := range modelFilters { @@ -764,7 +764,7 @@ func (j *JIMM) ListApplicationOffers(ctx context.Context, user *openfga.User, fi return offers, nil } -func (j *JIMM) listApplicationOffersForModel(ctx context.Context, user *openfga.User, m *dbmodel.Model, filters []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) { +func (j *JIMM) listApplicationOffersForModel(ctx context.Context, user *openfga.User, m *dbmodel.Model, filters []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) { const op = errors.Op("jimm.listApplicationOffersForModel") if err := j.Database.GetModel(ctx, m); err != nil { diff --git a/internal/jimm/applicationoffer_test.go b/internal/jimm/applicationoffer_test.go index 3f995450f..553596754 100644 --- a/internal/jimm/applicationoffer_test.go +++ b/internal/jimm/applicationoffer_test.go @@ -20,7 +20,6 @@ import ( "gopkg.in/macaroon.v2" "gorm.io/gorm" - "github.com/canonical/jimm/internal/constants" "github.com/canonical/jimm/internal/db" "github.com/canonical/jimm/internal/dbmodel" "github.com/canonical/jimm/internal/errors" @@ -664,7 +663,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { UUID: "00000000-0000-0000-0000-0000-0000000000001", API: &jimmtest.API{ GetApplicationOfferConsumeDetails_: func(ctx context.Context, user names.UserTag, details *jujuparams.ConsumeOfferDetails, v bakery.Version) error { - details.Offer = &jujuparams.ApplicationOfferDetails{ + details.Offer = &jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: names.NewModelTag(model.UUID.String).String(), OfferUUID: offer.UUID, OfferURL: offer.URL, @@ -676,10 +675,6 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Bindings: map[string]string{ - "key1": "value1", - "key2": "value2", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "alice@canonical.com", Access: "admin", @@ -690,15 +685,6 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { UserName: "bob@canonical.com", Access: "consume", }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value1", - "attr2": "value2", - }, - }}, } details.Macaroon = &macaroon.Macaroon{} details.ControllerInfo = &jujuparams.ExternalControllerInfo{ @@ -720,7 +706,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { about: "admin can get the application offer consume details ", user: u, details: jujuparams.ConsumeOfferDetails{ - Offer: &jujuparams.ApplicationOfferDetails{ + Offer: &jujuparams.ApplicationOfferDetailsV5{ OfferURL: "test-offer-url", }, }, @@ -731,7 +717,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { Addrs: []string{"test-public-address"}, }, Macaroon: &macaroon.Macaroon{}, - Offer: &jujuparams.ApplicationOfferDetails{ + Offer: &jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: names.NewModelTag(model.UUID.String).String(), OfferUUID: offer.UUID, OfferURL: offer.URL, @@ -743,10 +729,6 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Bindings: map[string]string{ - "key1": "value1", - "key2": "value2", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "alice@canonical.com", Access: "admin", @@ -760,22 +742,13 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { UserName: "everyone@external", Access: "read", }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value1", - "attr2": "value2", - }, - }}, }, }, }, { about: "users with consume access can get the application offer consume details with filtered users", user: u2, details: jujuparams.ConsumeOfferDetails{ - Offer: &jujuparams.ApplicationOfferDetails{ + Offer: &jujuparams.ApplicationOfferDetailsV5{ OfferURL: "test-offer-url", }, }, @@ -786,7 +759,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { Addrs: []string{"test-public-address"}, }, Macaroon: &macaroon.Macaroon{}, - Offer: &jujuparams.ApplicationOfferDetails{ + Offer: &jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: names.NewModelTag(model.UUID.String).String(), OfferUUID: offer.UUID, OfferURL: offer.URL, @@ -798,10 +771,6 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Bindings: map[string]string{ - "key1": "value1", - "key2": "value2", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "bob@canonical.com", Access: "consume", @@ -809,22 +778,13 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { UserName: "everyone@external", Access: "read", }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value1", - "attr2": "value2", - }, - }}, }, }, }, { about: "user with read access cannot get application offer consume details", user: u1, details: jujuparams.ConsumeOfferDetails{ - Offer: &jujuparams.ApplicationOfferDetails{ + Offer: &jujuparams.ApplicationOfferDetailsV5{ OfferURL: "test-offer-url", }, }, @@ -833,7 +793,7 @@ func TestGetApplicationOfferConsumeDetails(t *testing.T) { about: "no such offer", user: u, details: jujuparams.ConsumeOfferDetails{ - Offer: &jujuparams.ApplicationOfferDetails{ + Offer: &jujuparams.ApplicationOfferDetailsV5{ OfferURL: "no-such-offer", }, }, @@ -873,7 +833,7 @@ func TestGetApplicationOffer(t *testing.T) { }, Dialer: &jimmtest.Dialer{ API: &jimmtest.API{ - GetApplicationOffer_: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetails) error { + GetApplicationOffer_: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetailsV5) error { details.ApplicationName = "test-app" details.CharmURL = "cs:test-app:17" details.Connections = []jujuparams.OfferConnection{{ @@ -882,7 +842,7 @@ func TestGetApplicationOffer(t *testing.T) { Username: "unknown", Endpoint: "test-endpoint", }} - details.ApplicationOfferDetails = jujuparams.ApplicationOfferDetails{ + details.ApplicationOfferDetailsV5 = jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: names.NewModelTag("00000000-0000-0000-0000-0000-0000000000003").String(), OfferUUID: "00000000-0000-0000-0000-0000-0000000000011", OfferURL: "test-offer-url", @@ -893,24 +853,6 @@ func TestGetApplicationOffer(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value1", - "attr2": "value2", - }, - Subnets: []jujuparams.Subnet{{ - SpaceTag: "test-remote-space", - VLANTag: 1024, - Status: constants.DEAD.String(), - }}, - }}, - Bindings: map[string]string{ - "key1": "value4", - "key2": "value5", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "alice@canonical.com", Access: string(jujuparams.OfferAdminAccess), @@ -1004,20 +946,6 @@ func TestGetApplicationOffer(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Spaces: []dbmodel.ApplicationOfferRemoteSpace{{ - ApplicationOfferID: 1, - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderID: "test-provider-id", - ProviderAttributes: dbmodel.Map{ - "attr1": "value1", - "attr2": "value2", - }, - }}, - Bindings: dbmodel.StringMap{ - "key1": "value1", - "key2": "value2", - }, Connections: []dbmodel.ApplicationOfferConnection{{ ApplicationOfferID: 1, SourceModelTag: "test-model-src", @@ -1041,14 +969,14 @@ func TestGetApplicationOffer(t *testing.T) { about string user *dbmodel.Identity offerURL string - expectedOfferDetails jujuparams.ApplicationOfferAdminDetails + expectedOfferDetails jujuparams.ApplicationOfferAdminDetailsV5 expectedError string }{{ about: "admin can get the application offer", user: u, offerURL: "test-offer-url", - expectedOfferDetails: jujuparams.ApplicationOfferAdminDetails{ - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ + expectedOfferDetails: jujuparams.ApplicationOfferAdminDetailsV5{ + ApplicationOfferDetailsV5: jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: names.NewModelTag(model.UUID.String).String(), OfferUUID: "00000000-0000-0000-0000-0000-0000000000011", OfferURL: "test-offer-url", @@ -1059,10 +987,6 @@ func TestGetApplicationOffer(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Bindings: map[string]string{ - "key1": "value4", - "key2": "value5", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "alice@canonical.com", Access: "admin", @@ -1070,20 +994,6 @@ func TestGetApplicationOffer(t *testing.T) { UserName: "eve@canonical.com", Access: "read", }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value1", - "attr2": "value2", - }, - Subnets: []jujuparams.Subnet{{ - SpaceTag: "test-remote-space", - VLANTag: 1024, - Status: constants.DEAD.String(), - }}, - }}, }, ApplicationName: "test-app", CharmURL: "cs:test-app:17", @@ -1098,8 +1008,8 @@ func TestGetApplicationOffer(t *testing.T) { about: "user with read access can get the application offer, but users and connections are filtered", user: u1, offerURL: "test-offer-url", - expectedOfferDetails: jujuparams.ApplicationOfferAdminDetails{ - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ + expectedOfferDetails: jujuparams.ApplicationOfferAdminDetailsV5{ + ApplicationOfferDetailsV5: jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: names.NewModelTag(model.UUID.String).String(), OfferUUID: "00000000-0000-0000-0000-0000-0000000000011", OfferURL: "test-offer-url", @@ -1110,28 +1020,10 @@ func TestGetApplicationOffer(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Bindings: map[string]string{ - "key1": "value4", - "key2": "value5", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "eve@canonical.com", Access: "read", }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value1", - "attr2": "value2", - }, - Subnets: []jujuparams.Subnet{{ - SpaceTag: "test-remote-space", - VLANTag: 1024, - Status: constants.DEAD.String(), - }}, - }}, }, ApplicationName: "test-app", CharmURL: "cs:test-app:17", @@ -1170,13 +1062,13 @@ func TestOffer(t *testing.T) { now := time.Now().UTC().Round(time.Millisecond) tests := []struct { about string - getApplicationOffer func(context.Context, *jujuparams.ApplicationOfferAdminDetails) error + getApplicationOffer func(context.Context, *jujuparams.ApplicationOfferAdminDetailsV5) error grantApplicationOfferAccess func(context.Context, string, names.UserTag, jujuparams.OfferAccessPermission) error offer func(context.Context, crossmodel.OfferURL, jujuparams.AddApplicationOffer) error createEnv func(*qt.C, db.Database, *openfga.OFGAClient) (dbmodel.Identity, jimm.AddApplicationOfferParams, dbmodel.ApplicationOffer, func(*qt.C, error)) }{{ about: "all ok", - getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetails) error { + getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetailsV5) error { details.ApplicationName = "test-app" details.CharmURL = "cs:test-app:17" details.Connections = []jujuparams.OfferConnection{{ @@ -1185,7 +1077,7 @@ func TestOffer(t *testing.T) { Username: "unknown", Endpoint: "test-endpoint", }} - details.ApplicationOfferDetails = jujuparams.ApplicationOfferDetails{ + details.ApplicationOfferDetailsV5 = jujuparams.ApplicationOfferDetailsV5{ OfferUUID: "00000000-0000-0000-0000-0000-0000000000004", OfferURL: "test-offer-url", ApplicationDescription: "a test app offering", @@ -1195,24 +1087,6 @@ func TestOffer(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value1", - "attr2": "value2", - }, - Subnets: []jujuparams.Subnet{{ - SpaceTag: "test-remote-space", - VLANTag: 1024, - Status: constants.ALIVE.String(), - }}, - }}, - Bindings: map[string]string{ - "key1": "value1", - "key2": "value2", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "alice", DisplayName: "alice, sister of eve", @@ -1312,20 +1186,6 @@ func TestOffer(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Spaces: []dbmodel.ApplicationOfferRemoteSpace{{ - ApplicationOfferID: 1, - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderID: "test-provider-id", - ProviderAttributes: dbmodel.Map{ - "attr1": "value1", - "attr2": "value2", - }, - }}, - Bindings: dbmodel.StringMap{ - "key1": "value1", - "key2": "value2", - }, Connections: []dbmodel.ApplicationOfferConnection{{ ApplicationOfferID: 1, SourceModelTag: "test-model-src", @@ -1339,7 +1199,7 @@ func TestOffer(t *testing.T) { }, }, { about: "controller returns an error when creating an offer", - getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetails) error { + getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetailsV5) error { return nil }, grantApplicationOfferAccess: func(context.Context, string, names.UserTag, jujuparams.OfferAccessPermission) error { @@ -1426,7 +1286,7 @@ func TestOffer(t *testing.T) { }, }, { about: "model not found", - getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetails) error { + getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetailsV5) error { return nil }, grantApplicationOfferAccess: func(context.Context, string, names.UserTag, jujuparams.OfferAccessPermission) error { @@ -1458,7 +1318,7 @@ func TestOffer(t *testing.T) { }, }, { about: "application not found", - getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetails) error { + getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetailsV5) error { return nil }, grantApplicationOfferAccess: func(context.Context, string, names.UserTag, jujuparams.OfferAccessPermission) error { @@ -1547,7 +1407,7 @@ func TestOffer(t *testing.T) { }, }, { about: "user not model admin", - getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetails) error { + getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetailsV5) error { return nil }, grantApplicationOfferAccess: func(context.Context, string, names.UserTag, jujuparams.OfferAccessPermission) error { @@ -1638,7 +1498,7 @@ func TestOffer(t *testing.T) { }, }, { about: "fail to fetch application offer details", - getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetails) error { + getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetailsV5) error { return errors.E("a silly error") }, grantApplicationOfferAccess: func(context.Context, string, names.UserTag, jujuparams.OfferAccessPermission) error { @@ -1725,7 +1585,7 @@ func TestOffer(t *testing.T) { }, }, { about: "controller returns `application offer already exists`", - getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetails) error { + getApplicationOffer: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetailsV5) error { return nil }, grantApplicationOfferAccess: func(context.Context, string, names.UserTag, jujuparams.OfferAccessPermission) error { @@ -1948,20 +1808,6 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Spaces: []dbmodel.ApplicationOfferRemoteSpace{{ - ApplicationOfferID: 1, - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderID: "test-provider-id", - ProviderAttributes: dbmodel.Map{ - "attr1": "value1", - "attr2": "value2", - }, - }}, - Bindings: dbmodel.StringMap{ - "key1": "value1", - "key2": "value2", - }, Connections: []dbmodel.ApplicationOfferConnection{{ ApplicationOfferID: 1, SourceModelTag: "test-model-src", @@ -1975,7 +1821,7 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { } api := &jimmtest.API{ - GetApplicationOffer_: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetails) error { + GetApplicationOffer_: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetailsV5) error { details.ApplicationName = "test-app" details.CharmURL = "cs:test-app:17" details.Connections = []jujuparams.OfferConnection{{ @@ -1984,7 +1830,7 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { Username: "unknown", Endpoint: "test-endpoint", }} - details.ApplicationOfferDetails = jujuparams.ApplicationOfferDetails{ + details.ApplicationOfferDetailsV5 = jujuparams.ApplicationOfferDetailsV5{ OfferUUID: "00000000-0000-0000-0000-0000-0000000000004", OfferURL: "test-offer-url", ApplicationDescription: "a test app offering", @@ -1994,24 +1840,6 @@ func TestOfferAssertOpenFGARelationsExist(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value1", - "attr2": "value2", - }, - Subnets: []jujuparams.Subnet{{ - SpaceTag: "test-remote-space", - VLANTag: 1024, - Status: constants.ALIVE.String(), - }}, - }}, - Bindings: map[string]string{ - "key1": "value1", - "key2": "value2", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "alice", DisplayName: "alice, sister of eve", @@ -2295,19 +2123,6 @@ func TestUpdateOffer(t *testing.T) { ApplicationName: "test-app", CharmURL: "cs:test-app:17", ApplicationDescription: "changed offer description", - Spaces: []dbmodel.ApplicationOfferRemoteSpace{{ - ApplicationOfferID: 1, - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderID: "test-provider-id", - ProviderAttributes: dbmodel.Map{ - "attr1": "value3", - "attr2": "value4"}, - }}, - Bindings: dbmodel.StringMap{ - "key1": "value4", - "key2": "value5", - }, Connections: []dbmodel.ApplicationOfferConnection{{ ApplicationOfferID: 1, SourceModelTag: "test-model-src", @@ -2359,7 +2174,7 @@ func TestUpdateOffer(t *testing.T) { Database: db, Dialer: &jimmtest.Dialer{ API: &jimmtest.API{ - GetApplicationOffer_: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetails) error { + GetApplicationOffer_: func(_ context.Context, details *jujuparams.ApplicationOfferAdminDetailsV5) error { details.ApplicationName = "test-app" details.CharmURL = "cs:test-app:17" details.Connections = []jujuparams.OfferConnection{{ @@ -2368,7 +2183,7 @@ func TestUpdateOffer(t *testing.T) { Username: "unknown", Endpoint: "test-endpoint", }} - details.ApplicationOfferDetails = jujuparams.ApplicationOfferDetails{ + details.ApplicationOfferDetailsV5 = jujuparams.ApplicationOfferDetailsV5{ OfferUUID: "00000000-0000-0000-0000-0000-0000000000011", OfferURL: "test-offer-url", ApplicationDescription: "changed offer description", @@ -2378,24 +2193,6 @@ func TestUpdateOffer(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value3", - "attr2": "value4", - }, - Subnets: []jujuparams.Subnet{{ - SpaceTag: "test-remote-space", - VLANTag: 1024, - Status: constants.DEAD.String(), - }}, - }}, - Bindings: map[string]string{ - "key1": "value4", - "key2": "value5", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "alice", DisplayName: "alice, sister of eve", @@ -2543,7 +2340,7 @@ func TestFindApplicationOffers(t *testing.T) { if test.expectedError == "" { c.Assert(err, qt.IsNil) if test.expectedOffer != nil { - details := test.expectedOffer.ToJujuApplicationOfferDetails() + details := test.expectedOffer.ToJujuApplicationOfferDetailsV5() if accessLevel != string(jujuparams.OfferAdminAccess) { details.Users = []jujuparams.OfferUserDetails{{ UserName: user.Name, @@ -2587,7 +2384,7 @@ func TestFindApplicationOffers(t *testing.T) { cmpopts.IgnoreTypes(gorm.Model{}), cmpopts.IgnoreTypes(dbmodel.Model{}), ), - []jujuparams.ApplicationOfferAdminDetails{details}, + []jujuparams.ApplicationOfferAdminDetailsV5{details}, ) } else { c.Assert(offers, qt.HasLen, 0) @@ -2686,11 +2483,11 @@ func TestListApplicationOffers(t *testing.T) { Database: db, Dialer: &jimmtest.Dialer{ API: &jimmtest.API{ - ListApplicationOffers_: func(_ context.Context, filters []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) { + ListApplicationOffers_: func(_ context.Context, filters []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) { switch filters[0].ModelName { case "model-1": - return []jujuparams.ApplicationOfferAdminDetails{{ - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ + return []jujuparams.ApplicationOfferAdminDetailsV5{{ + ApplicationOfferDetailsV5: jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: "00000011-0000-0000-0000-000000000001", OfferUUID: "00000012-0000-0000-0000-000000000001", OfferURL: "test-offer-url", @@ -2702,10 +2499,6 @@ func TestListApplicationOffers(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Bindings: map[string]string{ - "key1": "value1", - "key2": "value2", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "alice@canonical.com", Access: "admin", @@ -2716,15 +2509,6 @@ func TestListApplicationOffers(t *testing.T) { UserName: "bob@canonical.com", Access: "consume", }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value1", - "attr2": "value2", - }, - }}, }, ApplicationName: "application-1", CharmURL: "charm-1", @@ -2735,7 +2519,7 @@ func TestListApplicationOffers(t *testing.T) { Endpoint: "an-endpoint", }}, }, { - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ + ApplicationOfferDetailsV5: jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: "00000011-0000-0000-0000-000000000002", OfferUUID: "00000012-0000-0000-0000-000000000002", OfferURL: "test-offer-url", @@ -2747,10 +2531,6 @@ func TestListApplicationOffers(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Bindings: map[string]string{ - "key1": "value1", - "key2": "value2", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "alice@canonical.com", Access: "admin", @@ -2761,15 +2541,6 @@ func TestListApplicationOffers(t *testing.T) { UserName: "bob@canonical.com", Access: "consume", }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value1", - "attr2": "value2", - }, - }}, }, ApplicationName: "application-2", CharmURL: "charm-2", @@ -2781,8 +2552,8 @@ func TestListApplicationOffers(t *testing.T) { }}, }}, nil case "model-2": - return []jujuparams.ApplicationOfferAdminDetails{{ - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ + return []jujuparams.ApplicationOfferAdminDetailsV5{{ + ApplicationOfferDetailsV5: jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: "00000011-0000-0000-0000-000000000003", OfferUUID: "00000012-0000-0000-0000-000000000003", OfferURL: "test-offer-url", @@ -2794,10 +2565,6 @@ func TestListApplicationOffers(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Bindings: map[string]string{ - "key1": "value1", - "key2": "value2", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "alice@canonical.com", Access: "admin", @@ -2808,15 +2575,6 @@ func TestListApplicationOffers(t *testing.T) { UserName: "bob@canonical.com", Access: "consume", }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value1", - "attr2": "value2", - }, - }}, }, ApplicationName: "application-3", CharmURL: "charm-3", @@ -2896,8 +2654,8 @@ func TestListApplicationOffers(t *testing.T) { return offers[i].Users[j].UserName < offers[i].Users[k].UserName }) } - c.Check(offers, qt.DeepEquals, []jujuparams.ApplicationOfferAdminDetails{{ - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ + c.Check(offers, qt.DeepEquals, []jujuparams.ApplicationOfferAdminDetailsV5{{ + ApplicationOfferDetailsV5: jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: "00000011-0000-0000-0000-000000000003", OfferUUID: "00000012-0000-0000-0000-000000000003", OfferURL: "test-offer-url", @@ -2909,10 +2667,6 @@ func TestListApplicationOffers(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Bindings: map[string]string{ - "key1": "value1", - "key2": "value2", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "alice@canonical.com", Access: "admin", @@ -2923,15 +2677,6 @@ func TestListApplicationOffers(t *testing.T) { UserName: "eve@canonical.com", Access: "read", }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value1", - "attr2": "value2", - }, - }}, }, ApplicationName: "application-3", CharmURL: "charm-3", @@ -2942,7 +2687,7 @@ func TestListApplicationOffers(t *testing.T) { Endpoint: "an-endpoint", }}, }, { - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ + ApplicationOfferDetailsV5: jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: "00000011-0000-0000-0000-000000000001", OfferUUID: "00000012-0000-0000-0000-000000000001", OfferURL: "test-offer-url", @@ -2954,10 +2699,6 @@ func TestListApplicationOffers(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Bindings: map[string]string{ - "key1": "value1", - "key2": "value2", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "alice@canonical.com", Access: "admin", @@ -2968,15 +2709,6 @@ func TestListApplicationOffers(t *testing.T) { UserName: "eve@canonical.com", Access: "read", }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value1", - "attr2": "value2", - }, - }}, }, ApplicationName: "application-1", CharmURL: "charm-1", @@ -2987,7 +2719,7 @@ func TestListApplicationOffers(t *testing.T) { Endpoint: "an-endpoint", }}, }, { - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ + ApplicationOfferDetailsV5: jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: "00000011-0000-0000-0000-000000000002", OfferUUID: "00000012-0000-0000-0000-000000000002", OfferURL: "test-offer-url", @@ -2999,10 +2731,6 @@ func TestListApplicationOffers(t *testing.T) { Interface: "unknown", Limit: 1, }}, - Bindings: map[string]string{ - "key1": "value1", - "key2": "value2", - }, Users: []jujuparams.OfferUserDetails{{ UserName: "alice@canonical.com", Access: "admin", @@ -3013,15 +2741,6 @@ func TestListApplicationOffers(t *testing.T) { UserName: "eve@canonical.com", Access: "read", }}, - Spaces: []jujuparams.RemoteSpace{{ - CloudType: "test-cloud-type", - Name: "test-remote-space", - ProviderId: "test-provider-id", - ProviderAttributes: map[string]interface{}{ - "attr1": "value1", - "attr2": "value2", - }, - }}, }, ApplicationName: "application-2", CharmURL: "charm-2", diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index fbf22e097..8dc0c1484 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -264,11 +264,11 @@ type API interface { // FindApplicationOffers finds application offers that match the // filter. - FindApplicationOffers(context.Context, []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) + FindApplicationOffers(context.Context, []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) // GetApplicationOffer completes the given ApplicationOfferAdminDetails // structure. - GetApplicationOffer(context.Context, *jujuparams.ApplicationOfferAdminDetails) error + GetApplicationOffer(context.Context, *jujuparams.ApplicationOfferAdminDetailsV5) error // GetApplicationOfferConsumeDetails gets the details required to // consume an application offer @@ -292,7 +292,7 @@ type API interface { // ListApplicationOffers lists application offers that match the // filter. - ListApplicationOffers(context.Context, []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) + ListApplicationOffers(context.Context, []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) // ModelInfo fetches a model's ModelInfo. ModelInfo(context.Context, *jujuparams.ModelInfo) error diff --git a/internal/jimmtest/api.go b/internal/jimmtest/api.go index 97e17aa5b..07dc0c28b 100644 --- a/internal/jimmtest/api.go +++ b/internal/jimmtest/api.go @@ -133,15 +133,15 @@ type API struct { DestroyModel_ func(context.Context, names.ModelTag, *bool, *bool, *time.Duration, *time.Duration) error DumpModel_ func(context.Context, names.ModelTag, bool) (string, error) DumpModelDB_ func(context.Context, names.ModelTag) (map[string]interface{}, error) - FindApplicationOffers_ func(context.Context, []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) - GetApplicationOffer_ func(context.Context, *jujuparams.ApplicationOfferAdminDetails) error + FindApplicationOffers_ func(context.Context, []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) + GetApplicationOffer_ func(context.Context, *jujuparams.ApplicationOfferAdminDetailsV5) error GetApplicationOfferConsumeDetails_ func(context.Context, names.UserTag, *jujuparams.ConsumeOfferDetails, bakery.Version) error GrantApplicationOfferAccess_ func(context.Context, string, names.UserTag, jujuparams.OfferAccessPermission) error GrantCloudAccess_ func(context.Context, names.CloudTag, names.UserTag, string) error GrantJIMMModelAdmin_ func(context.Context, names.ModelTag) error GrantModelAccess_ func(context.Context, names.ModelTag, names.UserTag, jujuparams.UserAccessPermission) error IsBroken_ bool - ListApplicationOffers_ func(context.Context, []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) + ListApplicationOffers_ func(context.Context, []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) ModelInfo_ func(context.Context, *jujuparams.ModelInfo) error ModelStatus_ func(context.Context, *jujuparams.ModelStatus) error ModelSummaryWatcherNext_ func(context.Context, string) ([]jujuparams.ModelAbstract, error) @@ -267,14 +267,14 @@ func (a *API) DumpModelDB(ctx context.Context, mt names.ModelTag) (map[string]in return a.DumpModelDB_(ctx, mt) } -func (a *API) FindApplicationOffers(ctx context.Context, f []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) { +func (a *API) FindApplicationOffers(ctx context.Context, f []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) { if a.FindApplicationOffers_ == nil { return nil, errors.E(errors.CodeNotImplemented) } return a.FindApplicationOffers_(ctx, f) } -func (a *API) GetApplicationOffer(ctx context.Context, offer *jujuparams.ApplicationOfferAdminDetails) error { +func (a *API) GetApplicationOffer(ctx context.Context, offer *jujuparams.ApplicationOfferAdminDetailsV5) error { if a.GetApplicationOffer_ == nil { return errors.E(errors.CodeNotImplemented) } @@ -320,7 +320,7 @@ func (a *API) IsBroken() bool { return a.IsBroken_ } -func (a *API) ListApplicationOffers(ctx context.Context, f []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) { +func (a *API) ListApplicationOffers(ctx context.Context, f []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) { if a.ListApplicationOffers_ == nil { return nil, errors.E(errors.CodeNotImplemented) } diff --git a/internal/jimmtest/jimm_mock.go b/internal/jimmtest/jimm_mock.go index 6b598f344..244f2a8e5 100644 --- a/internal/jimmtest/jimm_mock.go +++ b/internal/jimmtest/jimm_mock.go @@ -46,7 +46,7 @@ type JIMM struct { DumpModel_ func(ctx context.Context, u *openfga.User, mt names.ModelTag, simplified bool) (string, error) DumpModelDB_ func(ctx context.Context, u *openfga.User, mt names.ModelTag) (map[string]interface{}, error) EarliestControllerVersion_ func(ctx context.Context) (version.Number, error) - FindApplicationOffers_ func(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) + FindApplicationOffers_ func(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) FindAuditEvents_ func(ctx context.Context, user *openfga.User, filter db.AuditLogFilter) ([]dbmodel.AuditLogEntry, error) ForEachCloud_ func(ctx context.Context, user *openfga.User, f func(*dbmodel.Cloud) error) error ForEachModel_ func(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error @@ -54,7 +54,7 @@ type JIMM struct { ForEachUserCloudCredential_ func(ctx context.Context, u *dbmodel.Identity, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error ForEachUserModel_ func(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error FullModelStatus_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, patterns []string) (*jujuparams.FullStatus, error) - GetApplicationOffer_ func(ctx context.Context, user *openfga.User, offerURL string) (*jujuparams.ApplicationOfferAdminDetails, error) + GetApplicationOffer_ func(ctx context.Context, user *openfga.User, offerURL string) (*jujuparams.ApplicationOfferAdminDetailsV5, error) GetApplicationOfferConsumeDetails_ func(ctx context.Context, user *openfga.User, details *jujuparams.ConsumeOfferDetails, v bakery.Version) error GetCloud_ func(ctx context.Context, u *openfga.User, tag names.CloudTag) (dbmodel.Cloud, error) GetCloudCredential_ func(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) @@ -76,7 +76,7 @@ type JIMM struct { IdentityModelDefaults_ func(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) InitiateMigration_ func(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec, targetControllerID uint) (jujuparams.InitiateMigrationResult, error) InitiateInternalMigration_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetController string) (jujuparams.InitiateMigrationResult, error) - ListApplicationOffers_ func(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) + ListApplicationOffers_ func(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) ListControllers_ func(ctx context.Context, user *openfga.User) ([]dbmodel.Controller, error) ListGroups_ func(ctx context.Context, user *openfga.User) ([]dbmodel.GroupEntry, error) ModelDefaultsForCloud_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) @@ -218,7 +218,7 @@ func (j *JIMM) EarliestControllerVersion(ctx context.Context) (version.Number, e } return j.EarliestControllerVersion_(ctx) } -func (j *JIMM) FindApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) { +func (j *JIMM) FindApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) { if j.FindApplicationOffers_ == nil { return nil, errors.E(errors.CodeNotImplemented) } @@ -266,7 +266,7 @@ func (j *JIMM) FullModelStatus(ctx context.Context, user *openfga.User, modelTag } return j.FullModelStatus_(ctx, user, modelTag, patterns) } -func (j *JIMM) GetApplicationOffer(ctx context.Context, user *openfga.User, offerURL string) (*jujuparams.ApplicationOfferAdminDetails, error) { +func (j *JIMM) GetApplicationOffer(ctx context.Context, user *openfga.User, offerURL string) (*jujuparams.ApplicationOfferAdminDetailsV5, error) { if j.GetApplicationOffer_ == nil { return nil, errors.E(errors.CodeNotImplemented) } @@ -394,7 +394,7 @@ func (j *JIMM) InitiateInternalMigration(ctx context.Context, user *openfga.User } return j.InitiateInternalMigration_(ctx, user, modelTag, targetController) } -func (j *JIMM) ListApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) { +func (j *JIMM) ListApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) { if j.ListApplicationOffers_ == nil { return nil, errors.E(errors.CodeNotImplemented) } diff --git a/internal/jujuapi/applicationoffers.go b/internal/jujuapi/applicationoffers.go index 245c91004..167e88687 100644 --- a/internal/jujuapi/applicationoffers.go +++ b/internal/jujuapi/applicationoffers.go @@ -114,7 +114,7 @@ func (r *controllerRoot) getConsumeDetails(ctx context.Context, user *openfga.Us } details := jujuparams.ConsumeOfferDetails{ - Offer: &jujuparams.ApplicationOfferDetails{ + Offer: &jujuparams.ApplicationOfferDetailsV5{ OfferURL: ourl.AsLocal().Path(), }, } @@ -125,9 +125,9 @@ func (r *controllerRoot) getConsumeDetails(ctx context.Context, user *openfga.Us } // ListApplicationOffers returns all offers matching the specified filters. -func (r *controllerRoot) ListApplicationOffers(ctx context.Context, args jujuparams.OfferFilters) (jujuparams.QueryApplicationOffersResults, error) { +func (r *controllerRoot) ListApplicationOffers(ctx context.Context, args jujuparams.OfferFilters) (jujuparams.QueryApplicationOffersResultsV5, error) { const op = errors.Op("jujuapi.ListApplicationOffers") - results := jujuparams.QueryApplicationOffersResults{} + results := jujuparams.QueryApplicationOffersResultsV5{} offers, err := r.jimm.ListApplicationOffers(ctx, r.user, args.Filters...) if err != nil { @@ -141,9 +141,9 @@ func (r *controllerRoot) ListApplicationOffers(ctx context.Context, args jujupar // FindApplicationOffers returns all offers matching the specified filters // as long as the user has read access to each offer. It also omits details // on users and connections. -func (r *controllerRoot) FindApplicationOffers(ctx context.Context, args jujuparams.OfferFilters) (jujuparams.QueryApplicationOffersResults, error) { +func (r *controllerRoot) FindApplicationOffers(ctx context.Context, args jujuparams.OfferFilters) (jujuparams.QueryApplicationOffersResultsV5, error) { const op = errors.Op("jujuapi.FindApplicationOffers") - results := jujuparams.QueryApplicationOffersResults{} + results := jujuparams.QueryApplicationOffersResultsV5{} offers, err := r.jimm.FindApplicationOffers(ctx, r.user, args.Filters...) if err != nil { diff --git a/internal/jujuapi/applicationoffers_test.go b/internal/jujuapi/applicationoffers_test.go index 10624bd1c..41f305eed 100644 --- a/internal/jujuapi/applicationoffers_test.go +++ b/internal/jujuapi/applicationoffers_test.go @@ -112,7 +112,7 @@ func (s *applicationOffersSuite) TestGetConsumeDetails(c *gc.C) { return details.Offer.Users[i].UserName < details.Offer.Users[j].UserName }) c.Check(details, gc.DeepEquals, jujuparams.ConsumeOfferDetails{ - Offer: &jujuparams.ApplicationOfferDetails{ + Offer: &jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: s.Model.Tag().String(), OfferURL: ourl.Path(), OfferName: "test-offer", @@ -156,7 +156,7 @@ func (s *applicationOffersSuite) TestGetConsumeDetails(c *gc.C) { return details.Offer.Users[j].UserName < details.Offer.Users[k].UserName }) c.Check(details, gc.DeepEquals, jujuparams.ConsumeOfferDetails{ - Offer: &jujuparams.ApplicationOfferDetails{ + Offer: &jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: s.Model.Tag().String(), OfferURL: ourl.Path(), OfferName: "test-offer", diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index ac8b84a8e..24207c542 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -45,7 +45,7 @@ type JIMM interface { DumpModel(ctx context.Context, u *openfga.User, mt names.ModelTag, simplified bool) (string, error) DumpModelDB(ctx context.Context, u *openfga.User, mt names.ModelTag) (map[string]interface{}, error) EarliestControllerVersion(ctx context.Context) (version.Number, error) - FindApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) + FindApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) FindAuditEvents(ctx context.Context, user *openfga.User, filter db.AuditLogFilter) ([]dbmodel.AuditLogEntry, error) ForEachCloud(ctx context.Context, user *openfga.User, f func(*dbmodel.Cloud) error) error ForEachModel(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error @@ -53,7 +53,7 @@ type JIMM interface { ForEachUserCloudCredential(ctx context.Context, u *dbmodel.Identity, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error ForEachUserModel(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error FullModelStatus(ctx context.Context, user *openfga.User, modelTag names.ModelTag, patterns []string) (*jujuparams.FullStatus, error) - GetApplicationOffer(ctx context.Context, user *openfga.User, offerURL string) (*jujuparams.ApplicationOfferAdminDetails, error) + GetApplicationOffer(ctx context.Context, user *openfga.User, offerURL string) (*jujuparams.ApplicationOfferAdminDetailsV5, error) GetApplicationOfferConsumeDetails(ctx context.Context, user *openfga.User, details *jujuparams.ConsumeOfferDetails, v bakery.Version) error GetCloud(ctx context.Context, u *openfga.User, tag names.CloudTag) (dbmodel.Cloud, error) GetCloudCredential(ctx context.Context, user *openfga.User, tag names.CloudCredentialTag) (*dbmodel.CloudCredential, error) @@ -74,7 +74,7 @@ type JIMM interface { ImportModel(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error InitiateInternalMigration(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetController string) (jujuparams.InitiateMigrationResult, error) InitiateMigration(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec, targetControllerID uint) (jujuparams.InitiateMigrationResult, error) - ListApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) + ListApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) ListGroups(ctx context.Context, user *openfga.User) ([]dbmodel.GroupEntry, error) ModelDefaultsForCloud(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) ModelInfo(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelInfo, error) diff --git a/internal/jujuapi/modelmanager_test.go b/internal/jujuapi/modelmanager_test.go index a8b6f7260..d65931031 100644 --- a/internal/jujuapi/modelmanager_test.go +++ b/internal/jujuapi/modelmanager_test.go @@ -34,7 +34,7 @@ import ( ofganames "github.com/canonical/jimm/internal/openfga/names" ) -const jujuVersion = "3.5-beta1" +const jujuVersion = "3.5-rc1" type modelManagerSuite struct { websocketSuite diff --git a/internal/jujuclient/applicationoffers.go b/internal/jujuclient/applicationoffers.go index fd1117cc5..6c1c78b81 100644 --- a/internal/jujuclient/applicationoffers.go +++ b/internal/jujuclient/applicationoffers.go @@ -64,14 +64,14 @@ func (c Connection) Offer(ctx context.Context, offerURL crossmodel.OfferURL, off // ListApplicationOffers lists ApplicationOffers on the controller matching // the given filters. ListApplicationOffers uses the ListApplicationOffers // procedure on the ApplicationOffers facade. -func (c Connection) ListApplicationOffers(ctx context.Context, filters []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) { +func (c Connection) ListApplicationOffers(ctx context.Context, filters []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) { const op = errors.Op("jujuclient.ListApplicationOffers") args := jujuparams.OfferFilters{ Filters: filters, } - var resp jujuparams.QueryApplicationOffersResults - err := c.CallHighestFacadeVersion(ctx, "ApplicationOffers", []int{4, 2}, "", "ListApplicationOffers", &args, &resp) + var resp jujuparams.QueryApplicationOffersResultsV5 + err := c.CallHighestFacadeVersion(ctx, "ApplicationOffers", []int{5, 4}, "", "ListApplicationOffers", &args, &resp) if err != nil { return nil, errors.E(op, jujuerrors.Cause(err)) } @@ -81,14 +81,14 @@ func (c Connection) ListApplicationOffers(ctx context.Context, filters []jujupar // FindApplicationOffers finds ApplicationOffers on the controller matching // the given filters. FindApplicationOffers uses the FindApplicationOffers // procedure on the ApplicationOffers facade. -func (c Connection) FindApplicationOffers(ctx context.Context, filters []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetails, error) { +func (c Connection) FindApplicationOffers(ctx context.Context, filters []jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) { const op = errors.Op("jujuclient.FindApplicationOffers") args := jujuparams.OfferFilters{ Filters: filters, } - var resp jujuparams.QueryApplicationOffersResults - err := c.CallHighestFacadeVersion(ctx, "ApplicationOffers", []int{4, 2}, "", "FindApplicationOffers", &args, &resp) + var resp jujuparams.QueryApplicationOffersResultsV5 + err := c.CallHighestFacadeVersion(ctx, "ApplicationOffers", []int{5, 4}, "", "FindApplicationOffers", &args, &resp) if err != nil { return nil, errors.E(op, jujuerrors.Cause(err)) } @@ -100,7 +100,7 @@ func (c Connection) FindApplicationOffers(ctx context.Context, filters []jujupar // OfferURL the rest of the structure will be filled in by the API request. // GetApplicationOffer uses the ApplicationOffers procedure on the // ApplicationOffers facade. -func (c Connection) GetApplicationOffer(ctx context.Context, info *jujuparams.ApplicationOfferAdminDetails) error { +func (c Connection) GetApplicationOffer(ctx context.Context, info *jujuparams.ApplicationOfferAdminDetailsV5) error { const op = errors.Op("jujuclient.GetApplicationOffer") args := jujuparams.OfferURLs{ OfferURLs: []string{info.OfferURL}, @@ -109,7 +109,7 @@ func (c Connection) GetApplicationOffer(ctx context.Context, info *jujuparams.Ap resp := jujuparams.ApplicationOffersResults{ Results: make([]jujuparams.ApplicationOfferResult, 1), } - err := c.CallHighestFacadeVersion(ctx, "ApplicationOffers", []int{4, 2}, "", "ApplicationOffers", &args, &resp) + err := c.CallHighestFacadeVersion(ctx, "ApplicationOffers", []int{5, 4}, "", "ApplicationOffers", &args, &resp) if err != nil { return errors.E(op, jujuerrors.Cause(err)) } @@ -137,7 +137,7 @@ func (c Connection) GrantApplicationOfferAccess(ctx context.Context, offerURL st resp := jujuparams.ErrorResults{ Results: make([]jujuparams.ErrorResult, 1), } - err := c.CallHighestFacadeVersion(ctx, "ApplicationOffers", []int{4, 2}, "", "ModifyOfferAccess", &args, &resp) + err := c.CallHighestFacadeVersion(ctx, "ApplicationOffers", []int{5, 4}, "", "ModifyOfferAccess", &args, &resp) if err != nil { return errors.E(op, jujuerrors.Cause(err)) } @@ -164,7 +164,7 @@ func (c Connection) RevokeApplicationOfferAccess(ctx context.Context, offerURL s resp := jujuparams.ErrorResults{ Results: make([]jujuparams.ErrorResult, 1), } - err := c.CallHighestFacadeVersion(ctx, "ApplicationOffers", []int{4, 2}, "", "ModifyOfferAccess", &args, &resp) + err := c.CallHighestFacadeVersion(ctx, "ApplicationOffers", []int{5, 4}, "", "ModifyOfferAccess", &args, &resp) if err != nil { return errors.E(op, jujuerrors.Cause(err)) } @@ -187,7 +187,7 @@ func (c Connection) DestroyApplicationOffer(ctx context.Context, offer string, f resp := jujuparams.ErrorResults{ Results: make([]jujuparams.ErrorResult, 1), } - err := c.CallHighestFacadeVersion(ctx, "ApplicationOffers", []int{4, 2}, "", "DestroyOffers", &args, &resp) + err := c.CallHighestFacadeVersion(ctx, "ApplicationOffers", []int{5, 4}, "", "DestroyOffers", &args, &resp) if err != nil { return errors.E(op, jujuerrors.Cause(err)) } diff --git a/internal/jujuclient/applicationoffers_test.go b/internal/jujuclient/applicationoffers_test.go index 0f78ec0b9..b2e1d599b 100644 --- a/internal/jujuclient/applicationoffers_test.go +++ b/internal/jujuclient/applicationoffers_test.go @@ -164,7 +164,7 @@ func (s *applicationoffersSuite) TestListApplicationOffersMatching(c *gc.C) { ) c.Assert(err, gc.Equals, nil) - var info jujuparams.ApplicationOfferAdminDetails + var info jujuparams.ApplicationOfferAdminDetailsV5 info.OfferURL = offerURL.String() err = s.API.GetApplicationOffer(ctx, &info) c.Assert(err, gc.Equals, nil) @@ -176,7 +176,7 @@ func (s *applicationoffersSuite) TestListApplicationOffersMatching(c *gc.C) { ModelName: s.modelInfo.Name, }}) c.Assert(err, gc.Equals, nil) - c.Check(offers, gc.DeepEquals, []jujuparams.ApplicationOfferAdminDetails{info}) + c.Check(offers, gc.DeepEquals, []jujuparams.ApplicationOfferAdminDetailsV5{info}) } func (s *applicationoffersSuite) TestListApplicationOffersNoMatch(c *gc.C) { @@ -280,7 +280,7 @@ func (s *applicationoffersSuite) TestFindApplicationOffersMatching(c *gc.C) { ) c.Assert(err, gc.Equals, nil) - var info jujuparams.ApplicationOfferAdminDetails + var info jujuparams.ApplicationOfferAdminDetailsV5 info.OfferURL = offerURL.String() err = s.API.GetApplicationOffer(ctx, &info) c.Assert(err, gc.Equals, nil) @@ -292,7 +292,7 @@ func (s *applicationoffersSuite) TestFindApplicationOffersMatching(c *gc.C) { ModelName: s.modelInfo.Name, }}) c.Assert(err, gc.Equals, nil) - c.Check(offers, gc.DeepEquals, []jujuparams.ApplicationOfferAdminDetails{info}) + c.Check(offers, gc.DeepEquals, []jujuparams.ApplicationOfferAdminDetailsV5{info}) } func (s *applicationoffersSuite) TestFindApplicationOffersNoMatch(c *gc.C) { @@ -380,7 +380,7 @@ func (s *applicationoffersSuite) TestGetApplicationOffer(c *gc.C) { ) c.Assert(err, gc.Equals, nil) - var info jujuparams.ApplicationOfferAdminDetails + var info jujuparams.ApplicationOfferAdminDetailsV5 info.OfferURL = offerURL.String() err = s.API.GetApplicationOffer(ctx, &info) c.Assert(err, gc.Equals, nil) @@ -392,8 +392,8 @@ func (s *applicationoffersSuite) TestGetApplicationOffer(c *gc.C) { }) c.Check(info.CharmURL, gc.Matches, `ch:amd64/quantal/wordpress-[0-9]*`) info.CharmURL = "" - c.Check(info, jc.DeepEquals, jujuparams.ApplicationOfferAdminDetails{ - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ + c.Check(info, jc.DeepEquals, jujuparams.ApplicationOfferAdminDetailsV5{ + ApplicationOfferDetailsV5: jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: names.NewModelTag(s.modelInfo.UUID).String(), OfferURL: offerURL.String(), OfferName: "test-offer", @@ -420,7 +420,7 @@ func (s *applicationoffersSuite) TestGetApplicationOffer(c *gc.C) { func (s *applicationoffersSuite) TestGetApplicationOfferNotFound(c *gc.C) { ctx := context.Background() - var info jujuparams.ApplicationOfferAdminDetails + var info jujuparams.ApplicationOfferAdminDetailsV5 info.OfferURL = "test-user@canonical.com/test-model.test-offer" err := s.API.GetApplicationOffer(ctx, &info) c.Assert(err, gc.ErrorMatches, `application offer "test-user@canonical.com/test-model.test-offer" not found`) @@ -466,7 +466,7 @@ func (s *applicationoffersSuite) TestGrantApplicationOfferAccess(c *gc.C) { err = s.API.GrantApplicationOfferAccess(ctx, offerURL.String(), names.NewUserTag("test-user-2@canonical.com"), jujuparams.OfferConsumeAccess) c.Assert(err, gc.Equals, nil) - var info jujuparams.ApplicationOfferAdminDetails + var info jujuparams.ApplicationOfferAdminDetailsV5 info.OfferURL = offerURL.String() err = s.API.GetApplicationOffer(ctx, &info) c.Assert(err, gc.Equals, nil) @@ -477,8 +477,8 @@ func (s *applicationoffersSuite) TestGrantApplicationOfferAccess(c *gc.C) { }) c.Check(info.CharmURL, gc.Matches, `ch:amd64/quantal/wordpress-[0-9]*`) info.CharmURL = "" - c.Check(info, jc.DeepEquals, jujuparams.ApplicationOfferAdminDetails{ - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ + c.Check(info, jc.DeepEquals, jujuparams.ApplicationOfferAdminDetailsV5{ + ApplicationOfferDetailsV5: jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: names.NewModelTag(s.modelInfo.UUID).String(), OfferURL: offerURL.String(), OfferName: "test-offer", @@ -553,7 +553,7 @@ func (s *applicationoffersSuite) TestRevokeApplicationOfferAccess(c *gc.C) { err = s.API.GrantApplicationOfferAccess(ctx, offerURL.String(), names.NewUserTag("test-user-2@canonical.com"), jujuparams.OfferConsumeAccess) c.Assert(err, gc.Equals, nil) - var info jujuparams.ApplicationOfferAdminDetails + var info jujuparams.ApplicationOfferAdminDetailsV5 info.OfferURL = offerURL.String() err = s.API.GetApplicationOffer(ctx, &info) c.Assert(err, gc.Equals, nil) @@ -565,8 +565,8 @@ func (s *applicationoffersSuite) TestRevokeApplicationOfferAccess(c *gc.C) { }) c.Check(info.CharmURL, gc.Matches, `ch:amd64/quantal/wordpress-[0-9]*`) info.CharmURL = "" - c.Check(info, jc.DeepEquals, jujuparams.ApplicationOfferAdminDetails{ - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ + c.Check(info, jc.DeepEquals, jujuparams.ApplicationOfferAdminDetailsV5{ + ApplicationOfferDetailsV5: jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: names.NewModelTag(s.modelInfo.UUID).String(), OfferURL: "test-user@canonical.com/test-model.test-offer", OfferName: "test-offer", @@ -604,8 +604,8 @@ func (s *applicationoffersSuite) TestRevokeApplicationOfferAccess(c *gc.C) { }) c.Check(info.CharmURL, gc.Matches, `ch:amd64/quantal/wordpress-[0-9]*`) info.CharmURL = "" - c.Check(info, jc.DeepEquals, jujuparams.ApplicationOfferAdminDetails{ - ApplicationOfferDetails: jujuparams.ApplicationOfferDetails{ + c.Check(info, jc.DeepEquals, jujuparams.ApplicationOfferAdminDetailsV5{ + ApplicationOfferDetailsV5: jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: names.NewModelTag(s.modelInfo.UUID).String(), OfferURL: offerURL.String(), OfferName: "test-offer", @@ -735,7 +735,7 @@ func (s *applicationoffersSuite) TestGetApplicationOfferConsumeDetails(c *gc.C) c.Assert(err, gc.Equals, nil) var info jujuparams.ConsumeOfferDetails - info.Offer = &jujuparams.ApplicationOfferDetails{ + info.Offer = &jujuparams.ApplicationOfferDetailsV5{ OfferURL: offerURL.String(), } err = s.API.GetApplicationOfferConsumeDetails(ctx, names.NewUserTag("admin"), &info, bakery.Version2) @@ -748,7 +748,7 @@ func (s *applicationoffersSuite) TestGetApplicationOfferConsumeDetails(c *gc.C) return a.UserName < b.UserName } c.Check(info, jimmtest.CmpEquals(cmpopts.SortSlices(lessF)), jujuparams.ConsumeOfferDetails{ - Offer: &jujuparams.ApplicationOfferDetails{ + Offer: &jujuparams.ApplicationOfferDetailsV5{ SourceModelTag: names.NewModelTag(s.modelInfo.UUID).String(), OfferURL: "test-user@canonical.com/test-model.test-offer", OfferName: "test-offer", @@ -779,7 +779,7 @@ func (s *applicationoffersSuite) TestGetApplicationOfferConsumeDetails(c *gc.C) func (s *applicationoffersSuite) TestGetApplicationOfferConsumeDetailsNotFound(c *gc.C) { var info jujuparams.ConsumeOfferDetails - info.Offer = &jujuparams.ApplicationOfferDetails{ + info.Offer = &jujuparams.ApplicationOfferDetailsV5{ OfferURL: "test-user@canonical.com/test-model.test-offer", } err := s.API.GetApplicationOfferConsumeDetails(context.Background(), names.NewUserTag("test-user@canonical.com"), &info, bakery.Version2)