Compare commits

...

32 Commits

Author SHA1 Message Date
Jeff McCune
90f8eab816 (#144) Tidy go.mod and package.json 2024-04-25 19:14:20 -07:00
Jeff McCune
aee15f95e2 Merge pull request #145 from holos-run/jeff/144-organization-selector
(#144) Profile Button
2024-04-25 09:55:55 -07:00
Jeff McCune
1c540ac375 (#144) Profile Button and Organization Selector
This patch adds an organization "selector" that's really just a place
holder.  The active organization is the last element in the list
returned by the GetCallerOrganizations method for now.

The purpose is to make sure we have the structure in place for more than
one organizations without needing to implement full support for the
feature at this early stage.

The Angular frontend is expected to call the activeOrg() method of the
OrganizationService.  In the future this could store the state of which
organization the user has selected.  The purpose is to return an org id
to send as a request parameter for other requests.

Note this patch also implements refresh behavior.  The list of orgs is
fetched once on application load.  If there is no user, or the user has
zero orgs, the user is created and an organization is added with them as
an owner.  This is accompished using observable pipes.

The pipe is tied to a refresh behavior.  Clicking the org button
triggers the refresh behavior, which executes the pipe again and
notifies all subscribers.

This works quite well and should be idiomatic angular / rxjs.  Clicking
the button automatically updates the UI after making the necessary API
calls.
2024-04-25 09:55:13 -07:00
Jeff McCune
5b0e883ac9 (#144) Get or Create the orgranization
This patch adds the OrganizationService to the Angular front end and
displays a simple list of the organizations the user is a member of in
the profile card.

There isn't a service yet to return the currently selected
organization, but that could be a simple method to return the most
recent entry in the list until we put something more complicated in
place like local storage of what the user has selected.

It may make sense to put a database constraint on the number of
organizations until we implement the feature later, it's too early to do
so now, I just want to make sure it's possible to add later.
2024-04-25 07:02:17 -07:00
Jeff McCune
9a2519af71 (#144) Make the linter happy 2024-04-24 13:41:45 -07:00
Jeff McCune
9b9ff601c0 (#144) Call GetCallerClaims once instead of multiple times
Problem:
When loading the page the GetCallerClaims rpc method is called multiple
times unnecessarily.

Solution:
Use [shareReplay][1] to replay the last observable event for all
subscribers, including subscribers coming late to the party.

Result:
Network inspector in chrome indicates GetCallerClaims is called once and
only once.

[1]: https://rxjs.dev/api/operators/shareReplay
2024-04-24 12:44:44 -07:00
Jeff McCune
2f798296dc (#144) Profile Button
This patch adds a ProfileButton component which makes a ConnectRPC gRPC
call to the `holos.v1alpha1.UserService.GetCallerClaims` method and
renders the profile button based on the claims.

Note, in the network inspector there are two API calls to
`holos.v1alpha1.UserService.GetCallerClaims` which is unfortunate.  A
follow up patch might be good to fix this.
2024-04-24 12:23:54 -07:00
Jeff McCune
2b2ff63cad (#144) Connect /ui to ng serve for hot reload
Problem:
It's slow to build the angular app, compile it into the go executable,
copy it to the pod, then restart the server.

Solution:
Configure the mesh to route /ui to `ng serve` running on my local
host.

Result:
Navigating to https://jeff.app.dev.k2.holos.run/ui gets responses from
the ng development server.

Use:

    ng serve --host 0.0.0.0
2024-04-23 20:30:02 -07:00
Jeff McCune
3b135c09f3 (#144) Make a ConnectRPC call to the GetUserClaims method
This patch wires up an Angular RxJS Observable to the result of a gRPC
call to the `holos.v1alpha1.UserService.GetCallerClaims` method.

The implementation is a combination of [this connect example][1] and the
official [angular data][2] guide.

[1]: https://github.com/connectrpc/examples-es/tree/main/angular
[2]: https://angular.io/start/start-data#configuring-the-shippingcomponent-to-use-cartservice
2024-04-23 17:18:35 -07:00
Jeff McCune
28813eba5b (#126) v0.69.0 2024-04-23 10:24:58 -07:00
Jeff McCune
02ff765f54 Merge pull request #143 from holos-run/jeff/126-registration
(#126) Minimal API to register users and organizations
2024-04-23 09:00:07 -07:00
Jeff McCune
fe8a806132 (#126) Refactor to GetCallerX / CreateCallerX
This patch simplifies the user and organization registration and query
for the UI.  The pattern clients are expected to follow is to create if
the get fails.  For example, the following pseudo-go-code is the
expected calling convention:

    var entity *ent.User
    entity, err := Get()
    if err != nil {
      if ent.MaskNotFound(err) == nil {
        entity = Create()
      } else {
        return err
      }
    }
    return entity

This patch adds the following service methods.  For initial
registration, all input data comes from the id token claims of the
authenticated user.

```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" jeff.app.dev.k2.holos.run:443 list | xargs -n1 grpcurl -H "x-oidc-id-token: $(holos token)" jeff.app.dev.k2.holos.run:443 list
holos.v1alpha1.OrganizationService.CreateCallerOrganization
holos.v1alpha1.OrganizationService.GetCallerOrganizations
holos.v1alpha1.UserService.CreateCallerUser
holos.v1alpha1.UserService.GetCallerClaims
holos.v1alpha1.UserService.GetCallerUser
```
2024-04-23 08:43:23 -07:00
Jeff McCune
6626d58301 (#126) Add OrganizationService
Next step after this is to simplify the calling convention to a get
followed by a create if the get fails.
2024-04-23 05:28:07 -07:00
Jeff McCune
cb0911e890 (#126) Add holos.v1alpha1.UserService.GetUser
❯ grpcurl -H "x-oidc-id-token: $(holos token)" jeff.app.dev.k2.holos.run:443 holos.v1alpha1.UserService.GetUser
{
  "user": {
    "id": "018f07f4-4f9c-7b69-9d9e-07bf7bb4fe33",
    "email": "jeff@openinfrastructure.co",
    "name": "Jeff McCune",
    "timestamps": {
      "createdAt": "2024-04-22T22:36:42.780492Z",
      "updatedAt": "2024-04-22T22:36:42.780492Z"
    }
  }
}
2024-04-22 16:49:25 -07:00
Jeff McCune
3745a68dc5 (#126) Add unique index on user iss sub fields
The server will frequently look up the user record given the iss and sub
claims from the id token, index them and make sure the combination of
the two is unique.
2024-04-22 16:14:40 -07:00
Jeff McCune
fd64830476 (#126) Rename HolosService to UserService
Organize services by the resource they manage.
2024-04-22 16:05:32 -07:00
Jeff McCune
1ee0fa9c1f (#126) User Registration via API
With this patch user registration works with grpcurl.  Nothing in the
web UI yet.

```
grpcurl -H "x-oidc-id-token: $(holos token)" jeff.app.dev.k2.holos.run:443 holos.v1alpha1.HolosService.RegisterUser
```

Cannot register twice:

```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" jeff.app.dev.k2.holos.run:443 holos.v1alpha1.HolosService.RegisterUser
ERROR:
  Code: FailedPrecondition
  Message: user.go:26: ent: constraint failed: ERROR: duplicate key value violates unique constraint "users_email_key" (SQLSTATE 23505)
```

GetUserClaims works though:

```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" jeff.app.dev.k2.holos.run:443 holos.v1alpha1.HolosService.GetUserClaims
{
  "iss": "https://login.ois.run",
  "sub": "261773693724656988",
  "email": "jeff@openinfrastructure.co",
  "emailVerified": true,
  "name": "Jeff McCune"
}
```
2024-04-22 15:38:07 -07:00
Jeff McCune
8fab325b0a (#126) Add gRPC reflection
So grpcurl works as expected:

```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" jeff.app.dev.k2.holos.run:443 list
holos.v1alpha1.HolosService
```
2024-04-22 15:02:08 -07:00
Jeff McCune
858ffad913 (#126) Add holos token command for grpcurl 2024-04-22 14:54:09 -07:00
Jeff McCune
62735b99e7 (#126) Update Tiltfile to use holos.run for dev
This patch updates the Tiltfile to use the holos.run domain which is
integrated with the default Gateway.
2024-04-22 13:42:18 -07:00
Jeff McCune
29ab9c6300 (#141) Install provisioner helper.rb from entrypoint
And add a script to reset the choria provisioner credentials and config.
2024-04-22 13:20:38 -07:00
Jeff McCune
debc01c7de (#141) Fix Incorrect Provisioning Token foo given
The `make-provisioner-jwt` incorrectly used the choria broker password
as the provisioning token.  In the reference [setup.sh][1] both the
token and the `broker_provisioning_password` are set to `s3cret` so I
confused the two, but they are actually different values.

This patch ensures the provisioning token configured in
`provisioner.yaml` matches the token embedded into the provisioning.jwt
file using `choria jwt provisioning` via the `make-provisioner-jwt`
script.

[1]: 6dbc8fd105/example/setup/templates/provisioner/provisioner.yaml (L6)
2024-04-22 12:31:10 -07:00
Jeff McCune
c07f35ecd6 (#141) Fix holos controller invalid websocket connection error
Problem:
When the ingress default Gateway AuthorizationPolicy/authpolicy-custom
rule is in place the choria machine room holos controller fails to
connect to the provisioner broker with the following error:

```
❯ holos controller run --config=agent.cfg
WARN[0000] Starting controller version 0.68.1 with config file /home/jeff/workspace/holos-run/holos/hack/choria/agent/agent.cfg  leader=false
WARN[0000] Switching to provisioning configuration due to build defaults and missing /home/jeff/workspace/holos-run/holos/hack/choria/agent/agent.cfg
WARN[0000] Setting anonymous TLS mode during provisioning  component=server connection=coffee.home identity=coffee.home
WARN[0000] Initial connection to the Broker failed on try 1: invalid websocket connection  component=server connection=coffee.home identity=coffee.home
WARN[0000] Initial connection to the Broker failed on try 2: invalid websocket connection  component=server connection=coffee.home identity=coffee.home
WARN[0002] Initial connection to the Broker failed on try 3: invalid websocket connection  component=server connection=coffee.home identity=coffee.home
```

This problem is caused because the provisioning token url is set to
`wss://jeff.provision.dev.k2.holos.run:443` which has the port number
specified.

Solution:
Follow the upstream istio guidance of [Writing Host Match Policies][1]
to match host headers with or without the port specified.

Result:
The controller is able to connect to the provisioner broker:

[1]: https://istio.io/latest/docs/ops/best-practices/security/#writing-host-match-policies
2024-04-22 12:31:10 -07:00
Jeff McCune
c8f528700c (#141) Fix error: do not know how to handle choria_provisioning purpose token
Solution:
remove the plugin.security.choria.ca setting
2024-04-22 12:30:16 -07:00
Jeff McCune
896248c237 (#141) Try and connect holos controller to the provisioner
Running into error:

time="2024-04-20T03:23:19Z" level=warning msg="Denying connection: verified error: do not know how to handle choria_provisioning purpose token, unverified error: <nil>" component=authentication remote="10.244.1.51:56338" stage=check
time="2024-04-20T03:23:19Z" level=error msg="192.168.2.21/10.244.1.51:56338 - wid:367 - authentication error" component=network_broker
2024-04-22 12:29:56 -07:00
Jeff McCune
74a181db21 (#133) Add missing Choria Provisioner deployment 2024-04-19 15:14:31 -07:00
Jeff McCune
ba10113342 (#133) Fix tls error when connecting to provisioner websocket
This problem fixes an error where the istio ingress gateway proxy failed
to verify the TLS certificate presented by the choria broker upstream
server.

    kubectl logs choria-broker-0

    level=error msg="websocket: TLS handshake error from 10.244.1.190:36142: remote error: tls: unknown certificate\n"

Istio ingress logs:

    kubectl -n istio-ingress logs -l app=istio-ingressgateway -f | grep --line-buffered '^{' | jq .

    "upstream_transport_failure_reason": "TLS_error:|268435581:SSL_routines:OPENSSL_internal:CERTIFICATE_VERIFY_FAILED:TLS_error_end:TLS_error_end"

Client curl output:

    curl https://jeff.provision.dev.k2.holos.run

    upstream connect error or disconnect/reset before headers. retried and the latest reset reason: remote connection failure, transport failure reason: TLS_error:|268435581:SSL routines:OPENSSL_i
nternal:CERTIFICATE_VERIFY_FAILED:TLS_error_end:TLS_error_end

Explanation of error:

Istio defaults to expecting a tls certificate matching the downstream
host/authority which isn't how we've configured Choria.

Refer to [ClientTLSSettings][1]

> A list of alternate names to verify the subject identity in the
> certificate. If specified, the proxy will verify that the server
> certificate’s subject alt name matches one of the specified values. If
> specified, this list overrides the value of subject_alt_names from the
> ServiceEntry. If unspecified, automatic validation of upstream presented
> certificate for new upstream connections will be done based on the
> downstream HTTP host/authority header, provided
> VERIFY_CERTIFICATE_AT_CLIENT and ENABLE_AUTO_SNI environmental variables
> are set to true.

[1]: https://istio.io/latest/docs/reference/config/networking/destination-rule/#ClientTLSSettings
2024-04-19 13:13:09 -07:00
Jeff McCune
eb0207c92e (#133) Choria Provisioner
This patch is a work in progress to configure the provisioner to connect
to the broker.  Services and deployments are prefixed with choria for
clarity.
2024-04-19 13:13:08 -07:00
Jeff McCune
0fbcee8119 (#133) Extend the life of the Platform Issuer CA
The platform issuer root CA was set to expire after 90 days, the default
value.  This is too short.

Extend the life of the root CA beyond 100 years.
2024-04-19 11:29:17 -07:00
Jeff McCune
ce8bc798f6 (#133) Exclude nats and provision hosts from the auth proxy
Problem:
The identity aware auth proxy attached to the default gateway is
blocking access to NATS and the Choria Provisioner cluster.

Solution:
Add configuration that causes the project hosts to get added to the
exclusion list of the AuthorizationPolicy/authproxy-custom rule.

Result:
Requests bypass the auth proxy and go straight to the backend.  The
rules look like:

    kubectl get authorizationpolicy authproxy-custom -o yaml

```yaml
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: authproxy-custom
  namespace: istio-ingress
  labels:
    app.kubernetes.io/name: authproxy-custom
    app.kubernetes.io/part-of: istio-ingressgateway
spec:
  action: CUSTOM
  provider:
    name: ingressauth
  rules:
  - to:
    - operation:
        notHosts:
        - login.ois.run
        - vault.core.ois.run
        - provision.holos.run
        - nats.holos.run
        - provision.dev.holos.run
        - nats.dev.holos.run
        - jeff.provision.dev.holos.run
        - jeff.nats.dev.holos.run
        - gary.provision.dev.holos.run
        - gary.nats.dev.holos.run
        - nate.provision.dev.holos.run
        - nate.nats.dev.holos.run
        - provision.k2.holos.run
        - nats.k2.holos.run
        - provision.dev.k2.holos.run
        - nats.dev.k2.holos.run
        - jeff.provision.dev.k2.holos.run
        - jeff.nats.dev.k2.holos.run
        - gary.provision.dev.k2.holos.run
        - gary.nats.dev.k2.holos.run
        - nate.provision.dev.k2.holos.run
        - nate.nats.dev.k2.holos.run
    when:
    - key: request.headers[x-oidc-id-token]
      notValues:
      - '*'
  selector:
    matchLabels:
      istio: ingressgateway
```
2024-04-19 06:32:11 -07:00
Jeff McCune
996195d651 (#137) Fix ArgoCD PKCE login small comment 2024-04-18 14:39:13 -07:00
Jeff McCune
f00b29d3a3 (#137) Fix ArgoCD PKCE login
This patch configures ArgoCD to log in via PKCE.

Note the changes are primarily in platform.site.cue and ensuring the
emailDomain is set properly.  Note too the redirect URL needs to be
`/pkce/verify` when PKCE is enabled.  Finally, if the setting is
reconfigured make sure to clear cookies otherwise the incorrect
`/auth/callback` path may be used.
2024-04-18 14:37:06 -07:00
96 changed files with 3917 additions and 998 deletions

View File

@@ -99,6 +99,7 @@ docker_build_with_restart(
'--listen-port={}'.format(listen_port),
'--oidc-issuer=https://login.ois.run',
'--oidc-audience=262096764402729854@holos_platform',
'--log-level=debug',
'--metrics-port={}'.format(metrics_port),
],
dockerfile='./hack/tilt/Dockerfile',
@@ -190,7 +191,7 @@ k8s_resource(
],
resource_deps=[compile_id],
links=[
link('https://{}.holos.dev.k2.ois.run/app/'.format(developer), "Holos Web UI")
link('https://{}.app.dev.k2.holos.run/ui/'.format(developer), "Holos Web UI")
],
)
@@ -200,11 +201,6 @@ k8s_resource(
new_name=auth_id,
objects=[
'{}:virtualservice'.format(holos_server),
'{}-allow-groups:authorizationpolicy'.format(holos_server),
'{}-allow-nothing:authorizationpolicy'.format(holos_server),
'{}-allow-well-known-paths:authorizationpolicy'.format(holos_server),
'{}-auth:authorizationpolicy'.format(holos_server),
'{}:requestauthentication'.format(holos_server),
],
)

View File

@@ -11,14 +11,14 @@ plugins:
out: service/gen
opt: paths=source_relative
- plugin: es
out: internal/frontend/holos/gen
out: internal/frontend/holos/src/app/gen
opt:
- target=ts
- plugin: connect-es
out: internal/frontend/holos/gen
out: internal/frontend/holos/src/app/gen
opt:
- target=ts
- plugin: connect-query
out: internal/frontend/holos/gen
out: internal/frontend/holos/src/app/gen
opt:
- target=ts

View File

@@ -2,7 +2,10 @@ package holos
import ap "security.istio.io/authorizationpolicy/v1"
// #AuthPolicyRules represents AuthorizationPolicy rules for hosts that need specialized treatment. Entries in this struct are exclused from the blank ingressauth AuthorizationPolicy governing the ingressgateway and included in a spcialized policy
// #AuthPolicyRules represents AuthorizationPolicy rules for hosts that need
// specialized treatment. Entries in this struct are excluded from
// AuthorizationPolicy/authproxy-custom in the istio-ingress namespace. Entries
// are added to their own AuthorizationPolicy.
#AuthPolicyRules: {
// AuthProxySpec represents the identity provider configuration
AuthProxySpec: #AuthProxySpec & #Platform.authproxy
@@ -14,6 +17,9 @@ import ap "security.istio.io/authorizationpolicy/v1"
name: Name
// slug is the resource name prefix
slug: string
// NoAuthorizationPolicy disables an AuthorizationPolicy for the host
NoAuthorizationPolicy: true | *false
// Refer to https://istio.io/latest/docs/reference/config/security/authorization-policy/#Rule
spec: ap.#AuthorizationPolicySpec & {
action: "CUSTOM"
@@ -25,11 +31,13 @@ import ap "security.istio.io/authorizationpolicy/v1"
objects: #APIObjects & {
for Host in hosts {
apiObjects: {
AuthorizationPolicy: "\(Host.slug)-custom": {
metadata: namespace: "istio-ingress"
metadata: name: "\(Host.slug)-custom"
spec: Host.spec
if Host.NoAuthorizationPolicy == false {
apiObjects: {
AuthorizationPolicy: "\(Host.slug)-custom": {
metadata: namespace: "istio-ingress"
metadata: name: "\(Host.slug)-custom"
spec: Host.spec
}
}
}
}

View File

@@ -1,13 +1,13 @@
package holos
let Namespace = "jeff-holos"
let Broker = "broker"
let Broker = "choria-broker"
spec: components: KubernetesObjectsList: [
#KubernetesObjects & {
_dependsOn: "prod-platform-issuer": _
metadata: name: "\(Namespace)-broker"
metadata: name: "\(Namespace)-\(Broker)"
apiObjectMap: OBJECTS.apiObjectMap
},
]
@@ -31,9 +31,6 @@ let OBJECTS = #APIObjects & {
Broker,
"\(Broker).\(Namespace).svc",
"\(Broker).\(Namespace).svc.cluster.local",
"provision-\(Broker)",
"provision-\(Broker).\(Namespace).svc",
"provision-\(Broker).\(Namespace).svc.cluster.local",
"*.\(Broker)",
"*.\(Broker).\(Namespace).svc",
"*.\(Broker).\(Namespace).svc.cluster.local",

View File

@@ -0,0 +1,45 @@
package holos
let Namespace = "jeff-holos"
let Provisioner = "choria-provisioner"
spec: components: KubernetesObjectsList: [
#KubernetesObjects & {
_dependsOn: "prod-platform-issuer": _
metadata: name: "\(Namespace)-\(Provisioner)"
apiObjectMap: OBJECTS.apiObjectMap
},
]
let SelectorLabels = {
"app.kubernetes.io/instance": Provisioner
"app.kubernetes.io/name": Provisioner
}
let OBJECTS = #APIObjects & {
apiObjects: {
Certificate: "\(Provisioner)-tls": #Certificate & {
metadata: {
name: "\(Provisioner)-tls"
namespace: Namespace
labels: SelectorLabels
}
spec: {
commonName: "\(Provisioner).\(Namespace).svc.cluster.local"
dnsNames: [
Provisioner,
"\(Provisioner).\(Namespace).svc",
"\(Provisioner).\(Namespace).svc.cluster.local",
"*.\(Provisioner)",
"*.\(Provisioner).\(Namespace).svc",
"*.\(Provisioner).\(Namespace).svc.cluster.local",
]
issuerRef: kind: "ClusterIssuer"
issuerRef: name: "platform-issuer"
secretName: metadata.name
usages: ["signing", "key encipherment", "server auth", "client auth"]
}
}
}
}

View File

@@ -1,20 +1,20 @@
package holos
let Namespace = "jeff-holos"
let Broker = "broker"
let Broker = "choria-broker"
spec: components: KubernetesObjectsList: [
#KubernetesObjects & {
_dependsOn: "prod-secrets-stores": _
metadata: name: "\(Namespace)-broker"
metadata: name: "\(Namespace)-\(Broker)"
apiObjectMap: OBJECTS.apiObjectMap
},
]
let SelectorLabels = {
"app.kubernetes.io/instance": Broker
"app.kubernetes.io/name": Broker
"app.kubernetes.io/part-of": "choria"
"app.kubernetes.io/name": Broker
}
let Metadata = {
@@ -29,8 +29,8 @@ let OBJECTS = #APIObjects & {
metadata: name: "\(Broker)-tls"
metadata: namespace: Namespace
}
ExternalSecret: "choria-\(Broker)": #ExternalSecret & {
metadata: name: "choria-\(Broker)"
ExternalSecret: "\(Broker)": #ExternalSecret & {
metadata: name: Broker
metadata: namespace: Namespace
}
StatefulSet: "\(Broker)": {
@@ -92,7 +92,7 @@ let OBJECTS = #APIObjects & {
volumes: [
{
name: Broker
secret: secretName: "choria-\(Broker)"
secret: secretName: Broker
},
{
name: "\(Broker)-tls"
@@ -137,11 +137,20 @@ let OBJECTS = #APIObjects & {
}
}
DestinationRule: "\(Broker)-wss": #DestinationRule & {
metadata: Metadata
_decriptions: "Configures Istio to connect to Choria using a cert issued by the Platform Issuer"
metadata: Metadata
spec: host: "\(Broker).\(Namespace).svc.cluster.local"
spec: trafficPolicy: tls: {
credentialName: "istio-ingress-mtls-cert"
mode: "MUTUAL"
// subjectAltNames is important, otherwise istio will fail to verify the
// choria broker upstream server. make sure this matches a value
// present in the choria broker's cert.
//
// kubectl get secret choria-broker-tls -o json | jq --exit-status
// '.data | map_values(@base64d)' | jq .\"tls.crt\" -r | openssl x509
// -text -noout -in -
subjectAltNames: [spec.host]
}
}
VirtualService: "\(Broker)-wss": #VirtualService & {

View File

@@ -1,220 +0,0 @@
# build output from https://github.com/holos-run/holos-infra/blob/main/experiments/components/holos-saas/broker/build
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app.kubernetes.io/instance: broker
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: broker
app.kubernetes.io/version: 0.1.0
helm.sh/chart: broker-0.1.0
name: broker
---
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/instance: broker
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: broker
app.kubernetes.io/version: 0.1.0
helm.sh/chart: broker-0.1.0
name: broker
spec:
clusterIP: None
ports:
- appProtocol: tcp
name: tcp-nats
port: 4222
protocol: TCP
targetPort: tcp-nats
- appProtocol: tcp
name: tcp-cluster
port: 5222
protocol: TCP
targetPort: tcp-cluster
- appProtocol: https
name: https-wss
port: 443
protocol: TCP
targetPort: https-wss
selector:
app.kubernetes.io/instance: broker
app.kubernetes.io/name: broker
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/instance: broker
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: broker
app.kubernetes.io/version: 0.1.0
helm.sh/chart: broker-0.1.0
name: broker-lb
spec:
externalTrafficPolicy: Local
loadBalancerIP: 1.2.3.4
ports:
- appProtocol: tcp
name: tcp-nats
port: 4222
protocol: TCP
targetPort: tcp-nats
- appProtocol: https
name: https-wss
port: 443
protocol: TCP
targetPort: https-wss
selector:
app.kubernetes.io/instance: broker
app.kubernetes.io/name: broker
type: LoadBalancer
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app.kubernetes.io/instance: broker
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: broker
app.kubernetes.io/version: 0.1.0
helm.sh/chart: broker-0.1.0
name: broker
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/instance: broker
app.kubernetes.io/name: broker
serviceName: broker
template:
metadata:
labels:
app.kubernetes.io/instance: broker
app.kubernetes.io/name: broker
spec:
containers:
- command:
- choria
- broker
- run
- --config
- /etc/choria/broker.conf
image: registry.choria.io/choria/choria:latest
imagePullPolicy: Always
livenessProbe:
httpGet:
path: /healthz
port: http-stats
name: broker
ports:
- containerPort: 4222
name: tcp-nats
protocol: TCP
- containerPort: 4333
name: https-wss
protocol: TCP
- containerPort: 5222
name: tcp-cluster
protocol: TCP
- containerPort: 8222
name: http-stats
protocol: TCP
readinessProbe:
httpGet:
path: /healthz
port: http-stats
resources: {}
securityContext: {}
volumeMounts:
- mountPath: /etc/choria
name: broker
- mountPath: /etc/choria-tls
name: broker-tls
securityContext: {}
serviceAccountName: broker
volumes:
- name: broker
secret:
secretName: broker
- name: broker-tls
secret:
secretName: broker-tls
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: broker-tls
namespace: holos-dev
spec:
commonName: broker.holos-dev.svc.cluster.local
dnsNames:
- broker
- broker.holos-dev.svc
- broker.holos-dev.svc.cluster.local
- provision-broker
- provision-broker.holos-dev.svc
- provision-broker.holos-dev.svc.cluster.local
- '*.broker'
- '*.broker.holos-dev.svc'
- '*.broker.holos-dev.svc.cluster.local'
issuerRef:
kind: ClusterIssuer
name: cluster-issuer
secretName: broker-tls
usages:
- signing
- key encipherment
- server auth
- client auth
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: broker
spec:
dataFrom:
- extract:
key: kv//kube-namespace/holos-dev/broker
refreshInterval: 1h
secretStoreRef:
kind: SecretStore
name: core-vault
target:
creationPolicy: Owner
name: broker
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: broker-wss
namespace: holos-dev
spec:
host: broker.holos-dev.svc.cluster.local
trafficPolicy:
tls:
credentialName: istio-ingress-mtls-cert
mode: MUTUAL
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: broker-wss
namespace: holos-dev
spec:
gateways:
- istio-ingress/wildcard-pub-gw
hosts:
- provision.pub.k2.holos.run
http:
- route:
- destination:
host: broker.holos-dev.svc.cluster.local
port:
number: 443
tls:
mode: SIMPLE

View File

@@ -0,0 +1,18 @@
FROM registry.choria.io/choria/provisioner:latest
RUN curl -Lo nsc.zip https://github.com/nats-io/nsc/releases/download/v2.8.6/nsc-linux-amd64.zip &&\
unzip nsc.zip && \
mv nsc /usr/local/bin/nsc && \
chmod 755 /usr/local/bin/nsc && \
rm -f nsc.zip
# TODO: Add jwt executable
# TODO: Add helper executable
USER choria
ENV USER=choria
ENTRYPOINT ["/usr/sbin/choria-provisioner"]
# These two files are expected to be in the provisioner secret.
CMD ["--config=/etc/provisioner/provisioner.yaml", "--choria-config=/etc/provisioner/choria.cfg"]

View File

@@ -0,0 +1,82 @@
package holos
let Namespace = "jeff-holos"
let Provisioner = "choria-provisioner"
spec: components: KubernetesObjectsList: [
#KubernetesObjects & {
_dependsOn: "prod-secrets-stores": _
metadata: name: "\(Namespace)-\(Provisioner)"
apiObjectMap: OBJECTS.apiObjectMap
},
]
let SelectorLabels = {
"app.kubernetes.io/instance": Provisioner
"app.kubernetes.io/name": Provisioner
}
let Metadata = {
name: Provisioner
namespace: Namespace
labels: SelectorLabels
}
let OBJECTS = #APIObjects & {
apiObjects: {
ExternalSecret: "\(Provisioner)-tls": #ExternalSecret & {
metadata: name: "\(Provisioner)-tls"
metadata: namespace: Namespace
}
ExternalSecret: "\(Provisioner)": #ExternalSecret & {
metadata: name: Provisioner
metadata: namespace: Namespace
}
ServiceAccount: "\(Provisioner)": #ServiceAccount & {
metadata: Metadata
}
Deployment: "\(Provisioner)": {
metadata: Metadata
spec: {
selector: matchLabels: SelectorLabels
template: metadata: labels: SelectorLabels
template: spec: {
containers: [
{
name: Provisioner
command: ["bash", "/etc/provisioner/entrypoint"]
// skopeo inspect docker://registry.choria.io/choria/provisioner | jq .RepoTags
image: "registry.choria.io/choria/provisioner:0.15.1"
imagePullPolicy: "IfNotPresent"
resources: {}
securityContext: {}
volumeMounts: [
{
mountPath: "/etc/provisioner"
name: Provisioner
},
{
mountPath: "/etc/provisioner-tls"
name: "\(Provisioner)-tls"
},
]
},
]
securityContext: {}
serviceAccountName: Provisioner
volumes: [
{
name: Provisioner
secret: secretName: name
},
{
name: "\(Provisioner)-tls"
secret: secretName: name
},
]
}
}
}
}
}

View File

@@ -71,14 +71,15 @@ let IstioInject = [{op: "add", path: "/spec/template/metadata/labels/sidecar.ist
}
}
// Probably shouldn't use the authproxy struct and should instead define an identity provider struct.
let AuthProxySpec = #AuthProxySpec & #Platform.authproxy
let OAuthClient = #Platform.oauthClients.argocd.spec
let OIDCConfig = {
name: "Holos Platform"
issuer: AuthProxySpec.issuer
clientID: #Platform.argocd.clientID
requestedIDTokenClaims: groups: essential: true
requestedScopes: ["openid", "profile", "email", "groups", "urn:zitadel:iam:org:domain:primary:\(AuthProxySpec.orgDomain)"]
name: "Holos Platform"
issuer: OAuthClient.issuer
clientID: OAuthClient.clientID
requestedScopes: OAuthClient.scopesList
// Set redirect uri to https://argocd.example.com/pkce/verify
enablePKCEAuthentication: true
requestedIDTokenClaims: groups: essential: true
}

View File

@@ -18,6 +18,10 @@ _IngressAuthProxy: {
Domains: (#Platform.org.domain): _
Domains: "\(#ClusterName).\(#Platform.org.domain)": _
// TODO: This should be generated from ProjectHosts
Domains: "holos.run": _
Domains: "\(#ClusterName).holos.run": _
let Metadata = {
name: string
namespace: Namespace
@@ -271,11 +275,14 @@ _IngressAuthProxy: {
rules: [
{
to: [{
// Refer to https://istio.io/latest/docs/ops/best-practices/security/#writing-host-match-policies
operation: notHosts: [
// Never send requests for the login service through the authorizer, would block login.
AuthProxySpec.issuerHost,
"\(AuthProxySpec.issuerHost):*",
// Exclude hosts with specialized rules from the catch-all.
for x in _AuthPolicyRules.hosts {x.name},
for x in _AuthPolicyRules.hosts {"\(x.name):*"},
]
}]
when: [
@@ -298,7 +305,7 @@ _IngressAuthProxy: {
_AuthPolicyRules: #AuthPolicyRules & {
hosts: {
let Vault = "vault.core.ois.run"
(Vault): {
"\(Vault)": {
slug: "vault"
// Rules for when to route requests through the auth proxy
spec: rules: [
@@ -321,3 +328,20 @@ _AuthPolicyRules: #AuthPolicyRules & {
}
}
}
// Exclude project hosts from the auth proxy if configured to do so. The
// intended effect is to exclude the host from the blanket `authproxy-custom`
// AuthorizationPolicy rule _without_ adding a specialized AuthorizationPolicy
// for the same host. This has the effect of completely excluding the host from
// authorization policy.
for Project in _Projects {
let ProjectHosts = (#ProjectHosts & {project: Project}).Hosts
for FQDN, Host in ProjectHosts {
if Host.NoAuthorizationPolicy {
if Host.clusters[#ClusterName] != _|_ {
_AuthPolicyRules: hosts: "\(Host.fqdn)": NoAuthorizationPolicy: true
}
}
}
}

View File

@@ -43,9 +43,9 @@ _Projects: #Projects & {
// app is the holos web app and grpc api.
hosts: app: _
// provision is the choria broker provisioning system.
hosts: provision: _
hosts: provision: NoAuthorizationPolicy: true
// nats is the nats service holos controller machine room agents connect after provisioning.
hosts: nats: _
hosts: nats: NoAuthorizationPolicy: true
}
iam: {

View File

@@ -29,6 +29,7 @@ let OBJECTS = #APIObjects & {
metadata: name: PlatformIssuer
metadata: namespace: Namespace
spec: {
duration: "999999h"
isCA: true
commonName: PlatformIssuer
secretName: PlatformIssuer

View File

@@ -104,6 +104,8 @@ import "strings"
// Host is valid on all project clusters.
clusters: clusterMap
}
NoAuthorizationPolicy: host.NoAuthorizationPolicy
}
}
@@ -126,6 +128,10 @@ import "strings"
clusters: #ClusterMap
// hosts are always valid on the provisioner cluster
clusters: provisioner: _
// NoAuthorizationPolicy excludes the host from the auth proxy integrated with
// the default ingress Gateway.
NoAuthorizationPolicy: true | *false
}
#ClusterMap: [Name=string]: #Cluster & {name: Name}

View File

@@ -27,6 +27,9 @@ import (
project: #Project
let Project = project
// ProjectHosts represents all of the hosts associated with a project indexed
// by FQDN with #CertInfo values. Slice and dice this struct as needed to
// work with hosts in the platform.
ProjectHosts: (#ProjectHosts & {project: Project}).Hosts
// GatewayServers maps Gateway spec.servers #GatewayServer values indexed by stage then name.

View File

@@ -68,8 +68,13 @@ _Projects: #Projects
// #Cluster defines a cluster
#Cluster: name: string
// #Host defines a short hostname
#Host: name: string
#Host: {
// #Host defines a short hostname
name: string
// NoAuthorizationPolicy excludes the host from the auth proxy integrated with
// the default ingress Gateway.
NoAuthorizationPolicy: true | *false
}
#Environment: {
// name uniquely identifies the environment within the scope of the project.

1
go.mod
View File

@@ -5,6 +5,7 @@ go 1.21.5
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240401165935-b983156c5e99.1
connectrpc.com/connect v1.16.0
connectrpc.com/grpcreflect v1.2.0
connectrpc.com/validate v0.1.0
cuelang.org/go v0.8.0
entgo.io/ent v0.13.1

2
go.sum
View File

@@ -40,6 +40,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
connectrpc.com/connect v1.16.0 h1:rdtfQjZ0OyFkWPTegBNcH7cwquGAN1WzyJy80oFNibg=
connectrpc.com/connect v1.16.0/go.mod h1:XpZAduBQUySsb4/KO5JffORVkDI4B6/EYPi7N8xpNZw=
connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U=
connectrpc.com/grpcreflect v1.2.0/go.mod h1:nwSOKmE8nU5u/CidgHtPYk1PFI3U9ignz7iDMxOYkSY=
connectrpc.com/otelconnect v0.7.0 h1:ZH55ZZtcJOTKWWLy3qmL4Pam4RzRWBJFOqTPyAqCXkY=
connectrpc.com/otelconnect v0.7.0/go.mod h1:Bt2ivBymHZHqxvo4HkJ0EwHuUzQN6k2l0oH+mp/8nwc=
connectrpc.com/validate v0.1.0 h1:r55jirxMK7HO/xZwVHj3w2XkVFarsUM77ZDy367NtH4=

View File

@@ -1,5 +1,17 @@
Initialize machine room provisioning credentials
When you want the holos controller to provision while operating in the current
working directory, run:
1. `init-choria-provisioner-creds` to populate secrets in the Holos
Provisioner Cluster (not to be confused with the Choria Provisioner).
2. `make-provisioning-jwt` to issue a `provisioning.jwt` file for `holos
controller` to use.
3. `holos controller --config=agent.cfg` to read `provisioning.jwt` and write
the provisioned config file and credentials to the current directory.
Expect the controller to provision.
Setup Notes:
The holos server flag `--provisioner-seed` must match the issuer.seed value.

View File

@@ -6,6 +6,9 @@ export PROVISIONER_TOKEN="$(LC_ALL=C tr -dc "[:alpha:]" </dev/random | tr '[:upp
set -xeuo pipefail
# Make sure gomplate is available
gomplate --version
PARENT="$(cd $(dirname "$0") && pwd)"
TOPLEVEL="$(cd "${PARENT}" && git rev-parse --show-toplevel)"
: "${NAMESPACE:=jeff-holos}"
@@ -33,7 +36,7 @@ echo -n "${PROVISIONER_TOKEN}" > ./provisioner/token
# Provisioner signer
choria jwt keys ./provisioner/signer.seed ./provisioner/signer.public
choria jwt client ./provisioner/signer.jwt provisioner_signer ./issuer/issuer.seed \
--public-key "$(<provisioner/signer.public)" --server-provisioner --validity $((999*365))d --issuer
--public-key "$(<provisioner/signer.public)" --server-provisioner --validity $((100*365))d --issuer
# Provisioner Secret
mkdir -p provisioner/secret
@@ -57,5 +60,5 @@ choria jwt keys ./agents/signer.seed ./agents/signer.public
# Now save the secrets
holos create secret --append-hash=false --namespace $NAMESPACE choria-issuer --from-file=issuer
holos create secret --append-hash=false --namespace $NAMESPACE choria-broker --from-file=broker
holos create secret --append-hash=false --namespace $NAMESPACE choria-provisioner --from-file=provisioner
holos create secret --append-hash=false --namespace $NAMESPACE choria-provisioner --from-file=provisioner/secret
holos create secret --append-hash=false --namespace $NAMESPACE choria-agents --from-file=agents

View File

@@ -0,0 +1,50 @@
#! /bin/bash
#
# Make a provisioner.jwt and put it in the current directory.
#
# Use the provisioner.jwt with `holos controller --config=controller.cfg` which
# will read the jwt from the same directory as the config file.
#
# Refer to Arri's
# [setup.sh](https://github.com/ripienaar/machine-room-mvp/blob/main/example/setup/setup.sh#L41)
# And our own nites at https://github.com/holos-run/holos/issues/142
PARENT="$(cd $(dirname "$0") && pwd)"
OUTDIR="$(pwd)"
: "${NAMESPACE:=jeff-holos}"
tmpdir="$(mktemp -d)"
finish() {
[[ -d "$tmpdir" ]] && rm -rvf "$tmpdir"
}
trap finish EXIT
cd "$tmpdir"
set -xeuo pipefail
# e.g. jeff.provision.dev.k2.holos.run
#
kubectl -n $NAMESPACE get virtualservice choria-broker-wss -o json > vs.json
jq --exit-status -r '.spec.hosts[0]' vs.json > host
# Get the issuer.seed
holos -n $NAMESPACE get secret choria-issuer --to-file issuer.seed
# Get the provisioner token to embed in the provisioning.jwt file.
holos -n $NAMESPACE get secret choria-provisioner --to-file token
# The --token flag value must be the same value set in the token field of provisioner.yaml
# Refer to https://github.com/ripienaar/machine-room-mvp/blob/main/example/setup/setup.sh#L41
# Refer to https://github.com/ripienaar/machine-room-mvp/blob/main/example/setup/templates/provisioner/provisioner.yaml#L6
choria jwt prov provisioning.jwt "issuer.seed" \
--token "$(<token)" \
--urls wss://$(<host):443 \
--default \
--protocol-v2 \
--insecure \
--update \
--validity 30d \
--extensions '{}'
cp provisioning.jwt "${OUTDIR}/"

View File

@@ -0,0 +1,23 @@
#! /bin/bash
#
# This script resets the choria config for a Namespace
PARENT="$(cd $(dirname "$0") && pwd)"
: "${NAMESPACE:=jeff-holos}"
export NAMESPACE
set -xeuo pipefail
KUBECONFIG=$HOME/.holos/kubeconfig.provisioner kubectl delete secret -n jeff-holos choria-agents choria-broker choria-provisioner choria-issuer
"${PARENT}/init-choria-provisioner-creds"
stamp="$(date)"
kubectl -n $NAMESPACE annotate externalsecret choria-broker secret.holos.run/refresh="$stamp" --overwrite
kubectl -n $NAMESPACE annotate externalsecret choria-provisioner secret.holos.run/refresh="$stamp" --overwrite
kubectl -n $NAMESPACE wait --for='jsonpath={.status.conditions[?(@.type=="Ready")].status}=True' externalsecret choria-provisioner choria-broker
kubectl -n $NAMESPACE rollout restart statefulset choria-broker
kubectl -n $NAMESPACE rollout restart deployment choria-provisioner

View File

@@ -6,11 +6,13 @@ plugin.choria.network.client_port = 4222
plugin.choria.network.peer_port = 5222
plugin.choria.network.system.user = system
plugin.choria.network.system.password = system
plugin.choria.network.peers = nats://broker-0.broker:5222,nats://broker-1.broker:5222,nats://broker-2.broker:5222
plugin.choria.network.peers = nats://choria-broker-0.choria-broker:5222,nats://choria-broker-1.choria-broker:5222,nats://choria-broker-2.choria-broker:5222
plugin.choria.use_srv = false
plugin.choria.network.websocket_port = 4333
plugin.security.provider = choria
# NOTE: plugin.security.choria.ca must not be set or provisioning will fail
# with a unhandled choria_provisioning purpose token error
plugin.security.choria.certificate = /etc/choria-tls/tls.crt
plugin.security.choria.key = /etc/choria-tls/tls.key
plugin.security.choria.token_file = /etc/choria/broker.jwt

View File

@@ -4,4 +4,4 @@ plugin.security.choria.seed_file = /etc/provisioner/signer.seed
identity = provisioner_signer
plugin.choria.middleware_hosts = broker-0.broker:4222,broker-1.broker:4222,broker-2.broker:4222
plugin.choria.middleware_hosts = choria-broker-0.choria-broker:4222,choria-broker-1.choria-broker:4222,choria-broker-2.choria-broker:4222

View File

@@ -0,0 +1,9 @@
#! /bin/bash
#
set -xeuo pipefail
mkdir -p /home/choria/bin
install -m 0755 /etc/provisioner/helper.rb /home/choria/bin/helper.rb
exec /usr/sbin/choria-provisioner --config=/etc/provisioner/provisioner.yaml --choria-config=/etc/provisioner/choria.cfg

View File

@@ -0,0 +1,134 @@
#!/usr/bin/ruby
require "json"
require "yaml"
require "base64"
require "net/http"
require "openssl"
def parse_input
input = STDIN.read
begin
File.open("/tmp/request.json", "w") {|f| f.write(input)}
rescue Exception
end
request = JSON.parse(input)
request["inventory"] = JSON.parse(request["inventory"])
request
end
def validate!(request, reply)
if request["identity"] && request["identity"].length == 0
reply["msg"] = "No identity received in request"
reply["defer"] = true
return false
end
unless request["ed25519_pubkey"]
reply["msg"] = "No ed15519 public key received"
reply["defer"] = true
return false
end
unless request["ed25519_pubkey"]
reply["msg"] = "No ed15519 directory received"
reply["defer"] = true
return false
end
if request["ed25519_pubkey"]["directory"].length == 0
reply["msg"] = "No ed15519 directory received"
reply["defer"] = true
return false
end
true
end
def publish_reply(reply)
begin
File.open("/tmp/reply.json", "w") {|f| f.write(reply.to_json)}
rescue Exception
end
puts reply.to_json
end
def publish_reply!(reply)
publish_reply(reply)
exit
end
def set_config!(request, reply)
# stub data the helper will fetch from the saas
customers = {
"one" => {
:brokers => "nats://managed.example.net:9222", # whoever is the leader for this site
:site => "customer_one",
:source => {
:host => "nats://cust_one:s3cret@saas-nats.choria.local",
}
}
}
customer = request["jwt"]["extensions"]["customer"]
brokers = customers[customer][:brokers]
source = customers[customer][:source]
reply["configuration"].merge!(
"identity" => request["identity"],
"loglevel" => "warn",
"plugin.choria.server.provision" => "false",
"plugin.choria.middleware_hosts" => brokers,
"plugin.security.issuer.names" => "choria",
"plugin.security.issuer.choria.public" => "{{ .Env.ISSUER }}",
"plugin.security.provider" => "choria",
"plugin.security.choria.token_file" => File.join(request["ed25519_pubkey"]["directory"], "server.jwt"),
"plugin.security.choria.seed_file" => File.join(request["ed25519_pubkey"]["directory"], "server.seed"),
"machine_room.role" => "leader",
"machine_room.site" => customers[customer][:site],
"machine_room.source.host" => source[:host],
)
reply["server_claims"].merge!(
"exp" => 5*60*60*24*365,
"pub_subjects" => [">"],
"permissions" => {
"streams" => true,
"submission" => true,
"service_host" => true,
}
)
end
reply = {
"defer" => false,
"msg" => "",
"certificate" => "",
"ca" => "",
"configuration" => {},
"server_claims" => {}
}
begin
request = parse_input
reply["msg"] = "Validating"
unless validate!(request, reply)
publish_reply!(reply)
end
set_config!(request, reply)
reply["msg"] = "Done"
publish_reply!(reply)
rescue SystemExit
rescue Exception
reply["msg"] = "Unexpected failure during provisioning: %s: %s" % [$!.class, $!.to_s]
reply["defer"] = true
publish_reply!(reply)
end

View File

@@ -2,7 +2,8 @@ workers: 4
interval: 1m
logfile: /dev/stdout
loglevel: info
helper: /app/.venv/bin/helper
# The entrypoint script installs this helper script.
helper: /home/choria/bin/helper.rb
token: "{{ .Env.PROVISIONER_TOKEN }}"
choria_insecure: false
site: holos

View File

@@ -0,0 +1 @@
{{ .Env.PROVISIONER_TOKEN -}}

View File

@@ -101,15 +101,44 @@ spec:
gateways:
- istio-ingress/default
hosts:
- '{developer}.holos.dev.k2.ois.run'
- '{developer}.app.dev.k2.holos.run'
http:
- route:
- name: "coffee-ui"
match:
- uri:
prefix: "/ui"
route:
- destination:
host: coffee
port:
number: 4200
- name: "holos-api"
route:
- destination:
host: '{name}'
port:
number: {listen_port}
---
apiVersion: v1
kind: Service
metadata:
name: coffee
spec:
ports:
- protocol: TCP
port: 4200
---
apiVersion: v1
kind: Endpoints
metadata:
name: coffee
subsets:
- addresses:
- ip: 192.168.2.21
ports:
- port: 4200
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: holos
@@ -120,117 +149,6 @@ metadata:
imagePullSecrets:
- name: kube-system-ecr-image-pull-creds
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
labels:
app: '{name}'
holos.run/developer: '{developer}'
name: '{name}-allow-groups'
namespace: '{namespace}'
spec:
action: ALLOW
rules:
- when:
- key: request.auth.claims[groups]
values:
- holos-developer
- holos-developer@openinfrastructure.co
selector:
matchLabels:
holos.run/authz: dev-holos-sso
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: '{name}-allow-nothing'
namespace: '{namespace}'
labels:
app: '{name}'
holos.run/developer: '{developer}'
spec:
action: ALLOW
selector:
matchLabels:
holos.run/authz: dev-holos-sso
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: '{name}-allow-well-known-paths'
namespace: '{namespace}'
labels:
app: '{name}'
holos.run/developer: '{developer}'
spec:
action: ALLOW
rules:
- to:
- operation:
paths:
- /healthz
- /metrics
- /callbacks/github
selector:
matchLabels:
holos.run/authz: dev-holos-sso
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: '{name}-auth'
namespace: '{namespace}'
labels:
app: '{name}'
holos.run/developer: '{developer}'
spec:
action: CUSTOM
provider:
name: dev-holos-sso
rules:
- to:
- operation:
notPaths:
- /healthz
- /metrics
- /callbacks/github
when:
- key: request.headers[Authorization]
notValues:
- Bearer *
selector:
matchLabels:
holos.run/authz: dev-holos-sso
---
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: '{name}'
namespace: '{namespace}'
labels:
app: '{name}'
holos.run/developer: '{developer}'
spec:
jwtRules:
- audiences:
- https://sso.dev.holos.run
forwardOriginalToken: true
fromHeaders:
- name: x-auth-request-access-token
issuer: https://idex.core.ois.run
jwksUri: https://idex.core.ois.run/keys
- audiences:
- holos-cli
forwardOriginalToken: true
fromHeaders:
- name: authorization
prefix: 'Bearer '
issuer: https://idex.core.ois.run
jwksUri: https://idex.core.ois.run/keys
selector:
matchLabels:
holos.run/authz: dev-holos-sso
---
apiVersion: postgres-operator.crunchydata.com/v1beta1
kind: PGAdmin
metadata:

View File

@@ -15,6 +15,7 @@ import (
// New returns a new login command.
func New(cfg *holos.Config) *cobra.Command {
cmd := command.New("login")
cmd.Short = "log in by caching credentials"
var printClaims bool
config := token.NewConfig()

View File

@@ -13,6 +13,7 @@ import (
func New(cfg *holos.Config) *cobra.Command {
cmd := command.New("logout")
cmd.Short = "log out by deleting cached credentials"
cmd.RunE = func(c *cobra.Command, args []string) error {
if err := os.RemoveAll(token.CacheDir); err != nil {
return errors.Wrap(fmt.Errorf("could not logout: %w", err))

View File

@@ -16,6 +16,7 @@ import (
"github.com/holos-run/holos/internal/cli/logout"
"github.com/holos-run/holos/internal/cli/preflight"
"github.com/holos-run/holos/internal/cli/render"
"github.com/holos-run/holos/internal/cli/token"
"github.com/holos-run/holos/internal/cli/txtar"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/logger"
@@ -61,6 +62,7 @@ func New(cfg *holos.Config) *cobra.Command {
rootCmd.AddCommand(preflight.New(cfg))
rootCmd.AddCommand(login.New(cfg))
rootCmd.AddCommand(logout.New(cfg))
rootCmd.AddCommand(token.New(cfg))
// Maybe not needed?
rootCmd.AddCommand(txtar.New(cfg))

View File

@@ -0,0 +1,44 @@
package token
import (
"context"
"flag"
"fmt"
"log/slog"
"github.com/holos-run/holos/internal/cli/command"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/token"
"github.com/spf13/cobra"
)
// New returns a new login command.
func New(cfg *holos.Config) *cobra.Command {
cmd := command.New("token")
cmd.Short = "write id token to stdout"
cmd.Long = "Useful with curl / grpcurl -H $(holos token)"
config := token.NewConfig()
cmd.Flags().AddGoFlagSet(config.FlagSet())
fs := &flag.FlagSet{}
cmd.Flags().AddGoFlagSet(fs)
cmd.RunE = func(c *cobra.Command, args []string) error {
ctx := c.Context()
if ctx == nil {
ctx = context.Background()
}
token, err := token.Get(ctx, cfg.Logger(), config)
if err != nil {
slog.Error("could not get token", "err", err)
return fmt.Errorf("could not get token: %w", err)
}
fmt.Fprintf(cmd.OutOrStdout(), token.Bearer)
return nil
}
return cmd
}

View File

@@ -15,6 +15,7 @@ import (
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/holos-run/holos/internal/ent/organization"
"github.com/holos-run/holos/internal/ent/user"
)
@@ -315,6 +316,38 @@ func (c *OrganizationClient) GetX(ctx context.Context, id uuid.UUID) *Organizati
return obj
}
// QueryCreator queries the creator edge of a Organization.
func (c *OrganizationClient) QueryCreator(o *Organization) *UserQuery {
query := (&UserClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := o.ID
step := sqlgraph.NewStep(
sqlgraph.From(organization.Table, organization.FieldID, id),
sqlgraph.To(user.Table, user.FieldID),
sqlgraph.Edge(sqlgraph.M2O, false, organization.CreatorTable, organization.CreatorColumn),
)
fromV = sqlgraph.Neighbors(o.driver.Dialect(), step)
return fromV, nil
}
return query
}
// QueryUsers queries the users edge of a Organization.
func (c *OrganizationClient) QueryUsers(o *Organization) *UserQuery {
query := (&UserClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := o.ID
step := sqlgraph.NewStep(
sqlgraph.From(organization.Table, organization.FieldID, id),
sqlgraph.To(user.Table, user.FieldID),
sqlgraph.Edge(sqlgraph.M2M, false, organization.UsersTable, organization.UsersPrimaryKey...),
)
fromV = sqlgraph.Neighbors(o.driver.Dialect(), step)
return fromV, nil
}
return query
}
// Hooks returns the client hooks.
func (c *OrganizationClient) Hooks() []Hook {
return c.hooks.Organization
@@ -448,6 +481,22 @@ func (c *UserClient) GetX(ctx context.Context, id uuid.UUID) *User {
return obj
}
// QueryOrganizations queries the organizations edge of a User.
func (c *UserClient) QueryOrganizations(u *User) *OrganizationQuery {
query := (&OrganizationClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := u.ID
step := sqlgraph.NewStep(
sqlgraph.From(user.Table, user.FieldID, id),
sqlgraph.To(organization.Table, organization.FieldID),
sqlgraph.Edge(sqlgraph.M2M, true, user.OrganizationsTable, user.OrganizationsPrimaryKey...),
)
fromV = sqlgraph.Neighbors(u.driver.Dialect(), step)
return fromV, nil
}
return query
}
// Hooks returns the client hooks.
func (c *UserClient) Hooks() []Hook {
return c.hooks.User

View File

@@ -15,12 +15,21 @@ var (
{Name: "updated_at", Type: field.TypeTime},
{Name: "name", Type: field.TypeString, Unique: true},
{Name: "display_name", Type: field.TypeString},
{Name: "creator_id", Type: field.TypeUUID},
}
// OrganizationsTable holds the schema information for the "organizations" table.
OrganizationsTable = &schema.Table{
Name: "organizations",
Columns: OrganizationsColumns,
PrimaryKey: []*schema.Column{OrganizationsColumns[0]},
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "organizations_users_creator",
Columns: []*schema.Column{OrganizationsColumns[5]},
RefColumns: []*schema.Column{UsersColumns[0]},
OnDelete: schema.NoAction,
},
},
}
// UsersColumns holds the columns for the "users" table.
UsersColumns = []*schema.Column{
@@ -37,13 +46,49 @@ var (
Name: "users",
Columns: UsersColumns,
PrimaryKey: []*schema.Column{UsersColumns[0]},
Indexes: []*schema.Index{
{
Name: "user_iss_sub",
Unique: true,
Columns: []*schema.Column{UsersColumns[4], UsersColumns[5]},
},
},
}
// OrganizationUsersColumns holds the columns for the "organization_users" table.
OrganizationUsersColumns = []*schema.Column{
{Name: "organization_id", Type: field.TypeUUID},
{Name: "user_id", Type: field.TypeUUID},
}
// OrganizationUsersTable holds the schema information for the "organization_users" table.
OrganizationUsersTable = &schema.Table{
Name: "organization_users",
Columns: OrganizationUsersColumns,
PrimaryKey: []*schema.Column{OrganizationUsersColumns[0], OrganizationUsersColumns[1]},
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "organization_users_organization_id",
Columns: []*schema.Column{OrganizationUsersColumns[0]},
RefColumns: []*schema.Column{OrganizationsColumns[0]},
OnDelete: schema.Cascade,
},
{
Symbol: "organization_users_user_id",
Columns: []*schema.Column{OrganizationUsersColumns[1]},
RefColumns: []*schema.Column{UsersColumns[0]},
OnDelete: schema.Cascade,
},
},
}
// Tables holds all the tables in the schema.
Tables = []*schema.Table{
OrganizationsTable,
UsersTable,
OrganizationUsersTable,
}
)
func init() {
OrganizationsTable.ForeignKeys[0].RefTable = UsersTable
OrganizationUsersTable.ForeignKeys[0].RefTable = OrganizationsTable
OrganizationUsersTable.ForeignKeys[1].RefTable = UsersTable
}

View File

@@ -33,17 +33,22 @@ const (
// OrganizationMutation represents an operation that mutates the Organization nodes in the graph.
type OrganizationMutation struct {
config
op Op
typ string
id *uuid.UUID
created_at *time.Time
updated_at *time.Time
name *string
display_name *string
clearedFields map[string]struct{}
done bool
oldValue func(context.Context) (*Organization, error)
predicates []predicate.Organization
op Op
typ string
id *uuid.UUID
created_at *time.Time
updated_at *time.Time
name *string
display_name *string
clearedFields map[string]struct{}
creator *uuid.UUID
clearedcreator bool
users map[uuid.UUID]struct{}
removedusers map[uuid.UUID]struct{}
clearedusers bool
done bool
oldValue func(context.Context) (*Organization, error)
predicates []predicate.Organization
}
var _ ent.Mutation = (*OrganizationMutation)(nil)
@@ -294,6 +299,123 @@ func (m *OrganizationMutation) ResetDisplayName() {
m.display_name = nil
}
// SetCreatorID sets the "creator_id" field.
func (m *OrganizationMutation) SetCreatorID(u uuid.UUID) {
m.creator = &u
}
// CreatorID returns the value of the "creator_id" field in the mutation.
func (m *OrganizationMutation) CreatorID() (r uuid.UUID, exists bool) {
v := m.creator
if v == nil {
return
}
return *v, true
}
// OldCreatorID returns the old "creator_id" field's value of the Organization entity.
// If the Organization object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *OrganizationMutation) OldCreatorID(ctx context.Context) (v uuid.UUID, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldCreatorID is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldCreatorID requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldCreatorID: %w", err)
}
return oldValue.CreatorID, nil
}
// ResetCreatorID resets all changes to the "creator_id" field.
func (m *OrganizationMutation) ResetCreatorID() {
m.creator = nil
}
// ClearCreator clears the "creator" edge to the User entity.
func (m *OrganizationMutation) ClearCreator() {
m.clearedcreator = true
m.clearedFields[organization.FieldCreatorID] = struct{}{}
}
// CreatorCleared reports if the "creator" edge to the User entity was cleared.
func (m *OrganizationMutation) CreatorCleared() bool {
return m.clearedcreator
}
// CreatorIDs returns the "creator" edge IDs in the mutation.
// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use
// CreatorID instead. It exists only for internal usage by the builders.
func (m *OrganizationMutation) CreatorIDs() (ids []uuid.UUID) {
if id := m.creator; id != nil {
ids = append(ids, *id)
}
return
}
// ResetCreator resets all changes to the "creator" edge.
func (m *OrganizationMutation) ResetCreator() {
m.creator = nil
m.clearedcreator = false
}
// AddUserIDs adds the "users" edge to the User entity by ids.
func (m *OrganizationMutation) AddUserIDs(ids ...uuid.UUID) {
if m.users == nil {
m.users = make(map[uuid.UUID]struct{})
}
for i := range ids {
m.users[ids[i]] = struct{}{}
}
}
// ClearUsers clears the "users" edge to the User entity.
func (m *OrganizationMutation) ClearUsers() {
m.clearedusers = true
}
// UsersCleared reports if the "users" edge to the User entity was cleared.
func (m *OrganizationMutation) UsersCleared() bool {
return m.clearedusers
}
// RemoveUserIDs removes the "users" edge to the User entity by IDs.
func (m *OrganizationMutation) RemoveUserIDs(ids ...uuid.UUID) {
if m.removedusers == nil {
m.removedusers = make(map[uuid.UUID]struct{})
}
for i := range ids {
delete(m.users, ids[i])
m.removedusers[ids[i]] = struct{}{}
}
}
// RemovedUsers returns the removed IDs of the "users" edge to the User entity.
func (m *OrganizationMutation) RemovedUsersIDs() (ids []uuid.UUID) {
for id := range m.removedusers {
ids = append(ids, id)
}
return
}
// UsersIDs returns the "users" edge IDs in the mutation.
func (m *OrganizationMutation) UsersIDs() (ids []uuid.UUID) {
for id := range m.users {
ids = append(ids, id)
}
return
}
// ResetUsers resets all changes to the "users" edge.
func (m *OrganizationMutation) ResetUsers() {
m.users = nil
m.clearedusers = false
m.removedusers = nil
}
// Where appends a list predicates to the OrganizationMutation builder.
func (m *OrganizationMutation) Where(ps ...predicate.Organization) {
m.predicates = append(m.predicates, ps...)
@@ -328,7 +450,7 @@ func (m *OrganizationMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *OrganizationMutation) Fields() []string {
fields := make([]string, 0, 4)
fields := make([]string, 0, 5)
if m.created_at != nil {
fields = append(fields, organization.FieldCreatedAt)
}
@@ -341,6 +463,9 @@ func (m *OrganizationMutation) Fields() []string {
if m.display_name != nil {
fields = append(fields, organization.FieldDisplayName)
}
if m.creator != nil {
fields = append(fields, organization.FieldCreatorID)
}
return fields
}
@@ -357,6 +482,8 @@ func (m *OrganizationMutation) Field(name string) (ent.Value, bool) {
return m.Name()
case organization.FieldDisplayName:
return m.DisplayName()
case organization.FieldCreatorID:
return m.CreatorID()
}
return nil, false
}
@@ -374,6 +501,8 @@ func (m *OrganizationMutation) OldField(ctx context.Context, name string) (ent.V
return m.OldName(ctx)
case organization.FieldDisplayName:
return m.OldDisplayName(ctx)
case organization.FieldCreatorID:
return m.OldCreatorID(ctx)
}
return nil, fmt.Errorf("unknown Organization field %s", name)
}
@@ -411,6 +540,13 @@ func (m *OrganizationMutation) SetField(name string, value ent.Value) error {
}
m.SetDisplayName(v)
return nil
case organization.FieldCreatorID:
v, ok := value.(uuid.UUID)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetCreatorID(v)
return nil
}
return fmt.Errorf("unknown Organization field %s", name)
}
@@ -472,74 +608,134 @@ func (m *OrganizationMutation) ResetField(name string) error {
case organization.FieldDisplayName:
m.ResetDisplayName()
return nil
case organization.FieldCreatorID:
m.ResetCreatorID()
return nil
}
return fmt.Errorf("unknown Organization field %s", name)
}
// AddedEdges returns all edge names that were set/added in this mutation.
func (m *OrganizationMutation) AddedEdges() []string {
edges := make([]string, 0, 0)
edges := make([]string, 0, 2)
if m.creator != nil {
edges = append(edges, organization.EdgeCreator)
}
if m.users != nil {
edges = append(edges, organization.EdgeUsers)
}
return edges
}
// AddedIDs returns all IDs (to other nodes) that were added for the given edge
// name in this mutation.
func (m *OrganizationMutation) AddedIDs(name string) []ent.Value {
switch name {
case organization.EdgeCreator:
if id := m.creator; id != nil {
return []ent.Value{*id}
}
case organization.EdgeUsers:
ids := make([]ent.Value, 0, len(m.users))
for id := range m.users {
ids = append(ids, id)
}
return ids
}
return nil
}
// RemovedEdges returns all edge names that were removed in this mutation.
func (m *OrganizationMutation) RemovedEdges() []string {
edges := make([]string, 0, 0)
edges := make([]string, 0, 2)
if m.removedusers != nil {
edges = append(edges, organization.EdgeUsers)
}
return edges
}
// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with
// the given name in this mutation.
func (m *OrganizationMutation) RemovedIDs(name string) []ent.Value {
switch name {
case organization.EdgeUsers:
ids := make([]ent.Value, 0, len(m.removedusers))
for id := range m.removedusers {
ids = append(ids, id)
}
return ids
}
return nil
}
// ClearedEdges returns all edge names that were cleared in this mutation.
func (m *OrganizationMutation) ClearedEdges() []string {
edges := make([]string, 0, 0)
edges := make([]string, 0, 2)
if m.clearedcreator {
edges = append(edges, organization.EdgeCreator)
}
if m.clearedusers {
edges = append(edges, organization.EdgeUsers)
}
return edges
}
// EdgeCleared returns a boolean which indicates if the edge with the given name
// was cleared in this mutation.
func (m *OrganizationMutation) EdgeCleared(name string) bool {
switch name {
case organization.EdgeCreator:
return m.clearedcreator
case organization.EdgeUsers:
return m.clearedusers
}
return false
}
// ClearEdge clears the value of the edge with the given name. It returns an error
// if that edge is not defined in the schema.
func (m *OrganizationMutation) ClearEdge(name string) error {
switch name {
case organization.EdgeCreator:
m.ClearCreator()
return nil
}
return fmt.Errorf("unknown Organization unique edge %s", name)
}
// ResetEdge resets all changes to the edge with the given name in this mutation.
// It returns an error if the edge is not defined in the schema.
func (m *OrganizationMutation) ResetEdge(name string) error {
switch name {
case organization.EdgeCreator:
m.ResetCreator()
return nil
case organization.EdgeUsers:
m.ResetUsers()
return nil
}
return fmt.Errorf("unknown Organization edge %s", name)
}
// UserMutation represents an operation that mutates the User nodes in the graph.
type UserMutation struct {
config
op Op
typ string
id *uuid.UUID
created_at *time.Time
updated_at *time.Time
email *string
iss *string
sub *string
name *string
clearedFields map[string]struct{}
done bool
oldValue func(context.Context) (*User, error)
predicates []predicate.User
op Op
typ string
id *uuid.UUID
created_at *time.Time
updated_at *time.Time
email *string
iss *string
sub *string
name *string
clearedFields map[string]struct{}
organizations map[uuid.UUID]struct{}
removedorganizations map[uuid.UUID]struct{}
clearedorganizations bool
done bool
oldValue func(context.Context) (*User, error)
predicates []predicate.User
}
var _ ent.Mutation = (*UserMutation)(nil)
@@ -862,6 +1058,60 @@ func (m *UserMutation) ResetName() {
m.name = nil
}
// AddOrganizationIDs adds the "organizations" edge to the Organization entity by ids.
func (m *UserMutation) AddOrganizationIDs(ids ...uuid.UUID) {
if m.organizations == nil {
m.organizations = make(map[uuid.UUID]struct{})
}
for i := range ids {
m.organizations[ids[i]] = struct{}{}
}
}
// ClearOrganizations clears the "organizations" edge to the Organization entity.
func (m *UserMutation) ClearOrganizations() {
m.clearedorganizations = true
}
// OrganizationsCleared reports if the "organizations" edge to the Organization entity was cleared.
func (m *UserMutation) OrganizationsCleared() bool {
return m.clearedorganizations
}
// RemoveOrganizationIDs removes the "organizations" edge to the Organization entity by IDs.
func (m *UserMutation) RemoveOrganizationIDs(ids ...uuid.UUID) {
if m.removedorganizations == nil {
m.removedorganizations = make(map[uuid.UUID]struct{})
}
for i := range ids {
delete(m.organizations, ids[i])
m.removedorganizations[ids[i]] = struct{}{}
}
}
// RemovedOrganizations returns the removed IDs of the "organizations" edge to the Organization entity.
func (m *UserMutation) RemovedOrganizationsIDs() (ids []uuid.UUID) {
for id := range m.removedorganizations {
ids = append(ids, id)
}
return
}
// OrganizationsIDs returns the "organizations" edge IDs in the mutation.
func (m *UserMutation) OrganizationsIDs() (ids []uuid.UUID) {
for id := range m.organizations {
ids = append(ids, id)
}
return
}
// ResetOrganizations resets all changes to the "organizations" edge.
func (m *UserMutation) ResetOrganizations() {
m.organizations = nil
m.clearedorganizations = false
m.removedorganizations = nil
}
// Where appends a list predicates to the UserMutation builder.
func (m *UserMutation) Where(ps ...predicate.User) {
m.predicates = append(m.predicates, ps...)
@@ -1080,48 +1330,84 @@ func (m *UserMutation) ResetField(name string) error {
// AddedEdges returns all edge names that were set/added in this mutation.
func (m *UserMutation) AddedEdges() []string {
edges := make([]string, 0, 0)
edges := make([]string, 0, 1)
if m.organizations != nil {
edges = append(edges, user.EdgeOrganizations)
}
return edges
}
// AddedIDs returns all IDs (to other nodes) that were added for the given edge
// name in this mutation.
func (m *UserMutation) AddedIDs(name string) []ent.Value {
switch name {
case user.EdgeOrganizations:
ids := make([]ent.Value, 0, len(m.organizations))
for id := range m.organizations {
ids = append(ids, id)
}
return ids
}
return nil
}
// RemovedEdges returns all edge names that were removed in this mutation.
func (m *UserMutation) RemovedEdges() []string {
edges := make([]string, 0, 0)
edges := make([]string, 0, 1)
if m.removedorganizations != nil {
edges = append(edges, user.EdgeOrganizations)
}
return edges
}
// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with
// the given name in this mutation.
func (m *UserMutation) RemovedIDs(name string) []ent.Value {
switch name {
case user.EdgeOrganizations:
ids := make([]ent.Value, 0, len(m.removedorganizations))
for id := range m.removedorganizations {
ids = append(ids, id)
}
return ids
}
return nil
}
// ClearedEdges returns all edge names that were cleared in this mutation.
func (m *UserMutation) ClearedEdges() []string {
edges := make([]string, 0, 0)
edges := make([]string, 0, 1)
if m.clearedorganizations {
edges = append(edges, user.EdgeOrganizations)
}
return edges
}
// EdgeCleared returns a boolean which indicates if the edge with the given name
// was cleared in this mutation.
func (m *UserMutation) EdgeCleared(name string) bool {
switch name {
case user.EdgeOrganizations:
return m.clearedorganizations
}
return false
}
// ClearEdge clears the value of the edge with the given name. It returns an error
// if that edge is not defined in the schema.
func (m *UserMutation) ClearEdge(name string) error {
switch name {
}
return fmt.Errorf("unknown User unique edge %s", name)
}
// ResetEdge resets all changes to the edge with the given name in this mutation.
// It returns an error if the edge is not defined in the schema.
func (m *UserMutation) ResetEdge(name string) error {
switch name {
case user.EdgeOrganizations:
m.ResetOrganizations()
return nil
}
return fmt.Errorf("unknown User edge %s", name)
}

View File

@@ -11,6 +11,7 @@ import (
"entgo.io/ent/dialect/sql"
"github.com/gofrs/uuid"
"github.com/holos-run/holos/internal/ent/organization"
"github.com/holos-run/holos/internal/ent/user"
)
// Organization is the model entity for the Organization schema.
@@ -25,10 +26,46 @@ type Organization struct {
// Name holds the value of the "name" field.
Name string `json:"name,omitempty"`
// DisplayName holds the value of the "display_name" field.
DisplayName string `json:"display_name,omitempty"`
DisplayName string `json:"display_name,omitempty"`
// CreatorID holds the value of the "creator_id" field.
CreatorID uuid.UUID `json:"creator_id,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the OrganizationQuery when eager-loading is set.
Edges OrganizationEdges `json:"edges"`
selectValues sql.SelectValues
}
// OrganizationEdges holds the relations/edges for other nodes in the graph.
type OrganizationEdges struct {
// Creator holds the value of the creator edge.
Creator *User `json:"creator,omitempty"`
// Users holds the value of the users edge.
Users []*User `json:"users,omitempty"`
// loadedTypes holds the information for reporting if a
// type was loaded (or requested) in eager-loading or not.
loadedTypes [2]bool
}
// CreatorOrErr returns the Creator value or an error if the edge
// was not loaded in eager-loading, or loaded but was not found.
func (e OrganizationEdges) CreatorOrErr() (*User, error) {
if e.Creator != nil {
return e.Creator, nil
} else if e.loadedTypes[0] {
return nil, &NotFoundError{label: user.Label}
}
return nil, &NotLoadedError{edge: "creator"}
}
// UsersOrErr returns the Users value or an error if the edge
// was not loaded in eager-loading.
func (e OrganizationEdges) UsersOrErr() ([]*User, error) {
if e.loadedTypes[1] {
return e.Users, nil
}
return nil, &NotLoadedError{edge: "users"}
}
// scanValues returns the types for scanning values from sql.Rows.
func (*Organization) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
@@ -38,7 +75,7 @@ func (*Organization) scanValues(columns []string) ([]any, error) {
values[i] = new(sql.NullString)
case organization.FieldCreatedAt, organization.FieldUpdatedAt:
values[i] = new(sql.NullTime)
case organization.FieldID:
case organization.FieldID, organization.FieldCreatorID:
values[i] = new(uuid.UUID)
default:
values[i] = new(sql.UnknownType)
@@ -85,6 +122,12 @@ func (o *Organization) assignValues(columns []string, values []any) error {
} else if value.Valid {
o.DisplayName = value.String
}
case organization.FieldCreatorID:
if value, ok := values[i].(*uuid.UUID); !ok {
return fmt.Errorf("unexpected type %T for field creator_id", values[i])
} else if value != nil {
o.CreatorID = *value
}
default:
o.selectValues.Set(columns[i], values[i])
}
@@ -98,6 +141,16 @@ func (o *Organization) Value(name string) (ent.Value, error) {
return o.selectValues.Get(name)
}
// QueryCreator queries the "creator" edge of the Organization entity.
func (o *Organization) QueryCreator() *UserQuery {
return NewOrganizationClient(o.config).QueryCreator(o)
}
// QueryUsers queries the "users" edge of the Organization entity.
func (o *Organization) QueryUsers() *UserQuery {
return NewOrganizationClient(o.config).QueryUsers(o)
}
// Update returns a builder for updating this Organization.
// Note that you need to call Organization.Unwrap() before calling this method if this Organization
// was returned from a transaction, and the transaction was committed or rolled back.
@@ -132,6 +185,9 @@ func (o *Organization) String() string {
builder.WriteString(", ")
builder.WriteString("display_name=")
builder.WriteString(o.DisplayName)
builder.WriteString(", ")
builder.WriteString("creator_id=")
builder.WriteString(fmt.Sprintf("%v", o.CreatorID))
builder.WriteByte(')')
return builder.String()
}

View File

@@ -6,6 +6,7 @@ import (
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/gofrs/uuid"
)
@@ -22,8 +23,26 @@ const (
FieldName = "name"
// FieldDisplayName holds the string denoting the display_name field in the database.
FieldDisplayName = "display_name"
// FieldCreatorID holds the string denoting the creator_id field in the database.
FieldCreatorID = "creator_id"
// EdgeCreator holds the string denoting the creator edge name in mutations.
EdgeCreator = "creator"
// EdgeUsers holds the string denoting the users edge name in mutations.
EdgeUsers = "users"
// Table holds the table name of the organization in the database.
Table = "organizations"
// CreatorTable is the table that holds the creator relation/edge.
CreatorTable = "organizations"
// CreatorInverseTable is the table name for the User entity.
// It exists in this package in order to avoid circular dependency with the "user" package.
CreatorInverseTable = "users"
// CreatorColumn is the table column denoting the creator relation/edge.
CreatorColumn = "creator_id"
// UsersTable is the table that holds the users relation/edge. The primary key declared below.
UsersTable = "organization_users"
// UsersInverseTable is the table name for the User entity.
// It exists in this package in order to avoid circular dependency with the "user" package.
UsersInverseTable = "users"
)
// Columns holds all SQL columns for organization fields.
@@ -33,8 +52,15 @@ var Columns = []string{
FieldUpdatedAt,
FieldName,
FieldDisplayName,
FieldCreatorID,
}
var (
// UsersPrimaryKey and UsersColumn2 are the table columns denoting the
// primary key for the users relation (M2M).
UsersPrimaryKey = []string{"organization_id", "user_id"}
)
// ValidColumn reports if the column name is valid (part of the table columns).
func ValidColumn(column string) bool {
for i := range Columns {
@@ -85,3 +111,43 @@ func ByName(opts ...sql.OrderTermOption) OrderOption {
func ByDisplayName(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldDisplayName, opts...).ToFunc()
}
// ByCreatorID orders the results by the creator_id field.
func ByCreatorID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCreatorID, opts...).ToFunc()
}
// ByCreatorField orders the results by creator field.
func ByCreatorField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newCreatorStep(), sql.OrderByField(field, opts...))
}
}
// ByUsersCount orders the results by users count.
func ByUsersCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborsCount(s, newUsersStep(), opts...)
}
}
// ByUsers orders the results by users terms.
func ByUsers(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newUsersStep(), append([]sql.OrderTerm{term}, terms...)...)
}
}
func newCreatorStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(CreatorInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.M2O, false, CreatorTable, CreatorColumn),
)
}
func newUsersStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(UsersInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.M2M, false, UsersTable, UsersPrimaryKey...),
)
}

View File

@@ -6,6 +6,7 @@ import (
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/gofrs/uuid"
"github.com/holos-run/holos/internal/ent/predicate"
)
@@ -75,6 +76,11 @@ func DisplayName(v string) predicate.Organization {
return predicate.Organization(sql.FieldEQ(FieldDisplayName, v))
}
// CreatorID applies equality check predicate on the "creator_id" field. It's identical to CreatorIDEQ.
func CreatorID(v uuid.UUID) predicate.Organization {
return predicate.Organization(sql.FieldEQ(FieldCreatorID, v))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.Organization {
return predicate.Organization(sql.FieldEQ(FieldCreatedAt, v))
@@ -285,6 +291,72 @@ func DisplayNameContainsFold(v string) predicate.Organization {
return predicate.Organization(sql.FieldContainsFold(FieldDisplayName, v))
}
// CreatorIDEQ applies the EQ predicate on the "creator_id" field.
func CreatorIDEQ(v uuid.UUID) predicate.Organization {
return predicate.Organization(sql.FieldEQ(FieldCreatorID, v))
}
// CreatorIDNEQ applies the NEQ predicate on the "creator_id" field.
func CreatorIDNEQ(v uuid.UUID) predicate.Organization {
return predicate.Organization(sql.FieldNEQ(FieldCreatorID, v))
}
// CreatorIDIn applies the In predicate on the "creator_id" field.
func CreatorIDIn(vs ...uuid.UUID) predicate.Organization {
return predicate.Organization(sql.FieldIn(FieldCreatorID, vs...))
}
// CreatorIDNotIn applies the NotIn predicate on the "creator_id" field.
func CreatorIDNotIn(vs ...uuid.UUID) predicate.Organization {
return predicate.Organization(sql.FieldNotIn(FieldCreatorID, vs...))
}
// HasCreator applies the HasEdge predicate on the "creator" edge.
func HasCreator() predicate.Organization {
return predicate.Organization(func(s *sql.Selector) {
step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.Edge(sqlgraph.M2O, false, CreatorTable, CreatorColumn),
)
sqlgraph.HasNeighbors(s, step)
})
}
// HasCreatorWith applies the HasEdge predicate on the "creator" edge with a given conditions (other predicates).
func HasCreatorWith(preds ...predicate.User) predicate.Organization {
return predicate.Organization(func(s *sql.Selector) {
step := newCreatorStep()
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds {
p(s)
}
})
})
}
// HasUsers applies the HasEdge predicate on the "users" edge.
func HasUsers() predicate.Organization {
return predicate.Organization(func(s *sql.Selector) {
step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.Edge(sqlgraph.M2M, false, UsersTable, UsersPrimaryKey...),
)
sqlgraph.HasNeighbors(s, step)
})
}
// HasUsersWith applies the HasEdge predicate on the "users" edge with a given conditions (other predicates).
func HasUsersWith(preds ...predicate.User) predicate.Organization {
return predicate.Organization(func(s *sql.Selector) {
step := newUsersStep()
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds {
p(s)
}
})
})
}
// And groups predicates with the AND operator between them.
func And(predicates ...predicate.Organization) predicate.Organization {
return predicate.Organization(sql.AndPredicates(predicates...))

View File

@@ -14,6 +14,7 @@ import (
"entgo.io/ent/schema/field"
"github.com/gofrs/uuid"
"github.com/holos-run/holos/internal/ent/organization"
"github.com/holos-run/holos/internal/ent/user"
)
// OrganizationCreate is the builder for creating a Organization entity.
@@ -64,6 +65,12 @@ func (oc *OrganizationCreate) SetDisplayName(s string) *OrganizationCreate {
return oc
}
// SetCreatorID sets the "creator_id" field.
func (oc *OrganizationCreate) SetCreatorID(u uuid.UUID) *OrganizationCreate {
oc.mutation.SetCreatorID(u)
return oc
}
// SetID sets the "id" field.
func (oc *OrganizationCreate) SetID(u uuid.UUID) *OrganizationCreate {
oc.mutation.SetID(u)
@@ -78,6 +85,26 @@ func (oc *OrganizationCreate) SetNillableID(u *uuid.UUID) *OrganizationCreate {
return oc
}
// SetCreator sets the "creator" edge to the User entity.
func (oc *OrganizationCreate) SetCreator(u *User) *OrganizationCreate {
return oc.SetCreatorID(u.ID)
}
// AddUserIDs adds the "users" edge to the User entity by IDs.
func (oc *OrganizationCreate) AddUserIDs(ids ...uuid.UUID) *OrganizationCreate {
oc.mutation.AddUserIDs(ids...)
return oc
}
// AddUsers adds the "users" edges to the User entity.
func (oc *OrganizationCreate) AddUsers(u ...*User) *OrganizationCreate {
ids := make([]uuid.UUID, len(u))
for i := range u {
ids[i] = u[i].ID
}
return oc.AddUserIDs(ids...)
}
// Mutation returns the OrganizationMutation object of the builder.
func (oc *OrganizationCreate) Mutation() *OrganizationMutation {
return oc.mutation
@@ -146,6 +173,12 @@ func (oc *OrganizationCreate) check() error {
if _, ok := oc.mutation.DisplayName(); !ok {
return &ValidationError{Name: "display_name", err: errors.New(`ent: missing required field "Organization.display_name"`)}
}
if _, ok := oc.mutation.CreatorID(); !ok {
return &ValidationError{Name: "creator_id", err: errors.New(`ent: missing required field "Organization.creator_id"`)}
}
if _, ok := oc.mutation.CreatorID(); !ok {
return &ValidationError{Name: "creator", err: errors.New(`ent: missing required edge "Organization.creator"`)}
}
return nil
}
@@ -198,6 +231,39 @@ func (oc *OrganizationCreate) createSpec() (*Organization, *sqlgraph.CreateSpec)
_spec.SetField(organization.FieldDisplayName, field.TypeString, value)
_node.DisplayName = value
}
if nodes := oc.mutation.CreatorIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: false,
Table: organization.CreatorTable,
Columns: []string{organization.CreatorColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_node.CreatorID = nodes[0]
_spec.Edges = append(_spec.Edges, edge)
}
if nodes := oc.mutation.UsersIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: false,
Table: organization.UsersTable,
Columns: organization.UsersPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges = append(_spec.Edges, edge)
}
return _node, _spec
}
@@ -286,6 +352,18 @@ func (u *OrganizationUpsert) UpdateDisplayName() *OrganizationUpsert {
return u
}
// SetCreatorID sets the "creator_id" field.
func (u *OrganizationUpsert) SetCreatorID(v uuid.UUID) *OrganizationUpsert {
u.Set(organization.FieldCreatorID, v)
return u
}
// UpdateCreatorID sets the "creator_id" field to the value that was provided on create.
func (u *OrganizationUpsert) UpdateCreatorID() *OrganizationUpsert {
u.SetExcluded(organization.FieldCreatorID)
return u
}
// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field.
// Using this option is equivalent to using:
//
@@ -379,6 +457,20 @@ func (u *OrganizationUpsertOne) UpdateDisplayName() *OrganizationUpsertOne {
})
}
// SetCreatorID sets the "creator_id" field.
func (u *OrganizationUpsertOne) SetCreatorID(v uuid.UUID) *OrganizationUpsertOne {
return u.Update(func(s *OrganizationUpsert) {
s.SetCreatorID(v)
})
}
// UpdateCreatorID sets the "creator_id" field to the value that was provided on create.
func (u *OrganizationUpsertOne) UpdateCreatorID() *OrganizationUpsertOne {
return u.Update(func(s *OrganizationUpsert) {
s.UpdateCreatorID()
})
}
// Exec executes the query.
func (u *OrganizationUpsertOne) Exec(ctx context.Context) error {
if len(u.create.conflict) == 0 {
@@ -639,6 +731,20 @@ func (u *OrganizationUpsertBulk) UpdateDisplayName() *OrganizationUpsertBulk {
})
}
// SetCreatorID sets the "creator_id" field.
func (u *OrganizationUpsertBulk) SetCreatorID(v uuid.UUID) *OrganizationUpsertBulk {
return u.Update(func(s *OrganizationUpsert) {
s.SetCreatorID(v)
})
}
// UpdateCreatorID sets the "creator_id" field to the value that was provided on create.
func (u *OrganizationUpsertBulk) UpdateCreatorID() *OrganizationUpsertBulk {
return u.Update(func(s *OrganizationUpsert) {
s.UpdateCreatorID()
})
}
// Exec executes the query.
func (u *OrganizationUpsertBulk) Exec(ctx context.Context) error {
if u.create.err != nil {

View File

@@ -4,6 +4,7 @@ package ent
import (
"context"
"database/sql/driver"
"fmt"
"math"
@@ -13,15 +14,18 @@ import (
"github.com/gofrs/uuid"
"github.com/holos-run/holos/internal/ent/organization"
"github.com/holos-run/holos/internal/ent/predicate"
"github.com/holos-run/holos/internal/ent/user"
)
// OrganizationQuery is the builder for querying Organization entities.
type OrganizationQuery struct {
config
ctx *QueryContext
order []organization.OrderOption
inters []Interceptor
predicates []predicate.Organization
ctx *QueryContext
order []organization.OrderOption
inters []Interceptor
predicates []predicate.Organization
withCreator *UserQuery
withUsers *UserQuery
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -58,6 +62,50 @@ func (oq *OrganizationQuery) Order(o ...organization.OrderOption) *OrganizationQ
return oq
}
// QueryCreator chains the current query on the "creator" edge.
func (oq *OrganizationQuery) QueryCreator() *UserQuery {
query := (&UserClient{config: oq.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
if err := oq.prepareQuery(ctx); err != nil {
return nil, err
}
selector := oq.sqlQuery(ctx)
if err := selector.Err(); err != nil {
return nil, err
}
step := sqlgraph.NewStep(
sqlgraph.From(organization.Table, organization.FieldID, selector),
sqlgraph.To(user.Table, user.FieldID),
sqlgraph.Edge(sqlgraph.M2O, false, organization.CreatorTable, organization.CreatorColumn),
)
fromU = sqlgraph.SetNeighbors(oq.driver.Dialect(), step)
return fromU, nil
}
return query
}
// QueryUsers chains the current query on the "users" edge.
func (oq *OrganizationQuery) QueryUsers() *UserQuery {
query := (&UserClient{config: oq.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
if err := oq.prepareQuery(ctx); err != nil {
return nil, err
}
selector := oq.sqlQuery(ctx)
if err := selector.Err(); err != nil {
return nil, err
}
step := sqlgraph.NewStep(
sqlgraph.From(organization.Table, organization.FieldID, selector),
sqlgraph.To(user.Table, user.FieldID),
sqlgraph.Edge(sqlgraph.M2M, false, organization.UsersTable, organization.UsersPrimaryKey...),
)
fromU = sqlgraph.SetNeighbors(oq.driver.Dialect(), step)
return fromU, nil
}
return query
}
// First returns the first Organization entity from the query.
// Returns a *NotFoundError when no Organization was found.
func (oq *OrganizationQuery) First(ctx context.Context) (*Organization, error) {
@@ -245,17 +293,41 @@ func (oq *OrganizationQuery) Clone() *OrganizationQuery {
return nil
}
return &OrganizationQuery{
config: oq.config,
ctx: oq.ctx.Clone(),
order: append([]organization.OrderOption{}, oq.order...),
inters: append([]Interceptor{}, oq.inters...),
predicates: append([]predicate.Organization{}, oq.predicates...),
config: oq.config,
ctx: oq.ctx.Clone(),
order: append([]organization.OrderOption{}, oq.order...),
inters: append([]Interceptor{}, oq.inters...),
predicates: append([]predicate.Organization{}, oq.predicates...),
withCreator: oq.withCreator.Clone(),
withUsers: oq.withUsers.Clone(),
// clone intermediate query.
sql: oq.sql.Clone(),
path: oq.path,
}
}
// WithCreator tells the query-builder to eager-load the nodes that are connected to
// the "creator" edge. The optional arguments are used to configure the query builder of the edge.
func (oq *OrganizationQuery) WithCreator(opts ...func(*UserQuery)) *OrganizationQuery {
query := (&UserClient{config: oq.config}).Query()
for _, opt := range opts {
opt(query)
}
oq.withCreator = query
return oq
}
// WithUsers tells the query-builder to eager-load the nodes that are connected to
// the "users" edge. The optional arguments are used to configure the query builder of the edge.
func (oq *OrganizationQuery) WithUsers(opts ...func(*UserQuery)) *OrganizationQuery {
query := (&UserClient{config: oq.config}).Query()
for _, opt := range opts {
opt(query)
}
oq.withUsers = query
return oq
}
// GroupBy is used to group vertices by one or more fields/columns.
// It is often used with aggregate functions, like: count, max, mean, min, sum.
//
@@ -332,8 +404,12 @@ func (oq *OrganizationQuery) prepareQuery(ctx context.Context) error {
func (oq *OrganizationQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Organization, error) {
var (
nodes = []*Organization{}
_spec = oq.querySpec()
nodes = []*Organization{}
_spec = oq.querySpec()
loadedTypes = [2]bool{
oq.withCreator != nil,
oq.withUsers != nil,
}
)
_spec.ScanValues = func(columns []string) ([]any, error) {
return (*Organization).scanValues(nil, columns)
@@ -341,6 +417,7 @@ func (oq *OrganizationQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]
_spec.Assign = func(columns []string, values []any) error {
node := &Organization{config: oq.config}
nodes = append(nodes, node)
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
for i := range hooks {
@@ -352,9 +429,113 @@ func (oq *OrganizationQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]
if len(nodes) == 0 {
return nodes, nil
}
if query := oq.withCreator; query != nil {
if err := oq.loadCreator(ctx, query, nodes, nil,
func(n *Organization, e *User) { n.Edges.Creator = e }); err != nil {
return nil, err
}
}
if query := oq.withUsers; query != nil {
if err := oq.loadUsers(ctx, query, nodes,
func(n *Organization) { n.Edges.Users = []*User{} },
func(n *Organization, e *User) { n.Edges.Users = append(n.Edges.Users, e) }); err != nil {
return nil, err
}
}
return nodes, nil
}
func (oq *OrganizationQuery) loadCreator(ctx context.Context, query *UserQuery, nodes []*Organization, init func(*Organization), assign func(*Organization, *User)) error {
ids := make([]uuid.UUID, 0, len(nodes))
nodeids := make(map[uuid.UUID][]*Organization)
for i := range nodes {
fk := nodes[i].CreatorID
if _, ok := nodeids[fk]; !ok {
ids = append(ids, fk)
}
nodeids[fk] = append(nodeids[fk], nodes[i])
}
if len(ids) == 0 {
return nil
}
query.Where(user.IDIn(ids...))
neighbors, err := query.All(ctx)
if err != nil {
return err
}
for _, n := range neighbors {
nodes, ok := nodeids[n.ID]
if !ok {
return fmt.Errorf(`unexpected foreign-key "creator_id" returned %v`, n.ID)
}
for i := range nodes {
assign(nodes[i], n)
}
}
return nil
}
func (oq *OrganizationQuery) loadUsers(ctx context.Context, query *UserQuery, nodes []*Organization, init func(*Organization), assign func(*Organization, *User)) error {
edgeIDs := make([]driver.Value, len(nodes))
byID := make(map[uuid.UUID]*Organization)
nids := make(map[uuid.UUID]map[*Organization]struct{})
for i, node := range nodes {
edgeIDs[i] = node.ID
byID[node.ID] = node
if init != nil {
init(node)
}
}
query.Where(func(s *sql.Selector) {
joinT := sql.Table(organization.UsersTable)
s.Join(joinT).On(s.C(user.FieldID), joinT.C(organization.UsersPrimaryKey[1]))
s.Where(sql.InValues(joinT.C(organization.UsersPrimaryKey[0]), edgeIDs...))
columns := s.SelectedColumns()
s.Select(joinT.C(organization.UsersPrimaryKey[0]))
s.AppendSelect(columns...)
s.SetDistinct(false)
})
if err := query.prepareQuery(ctx); err != nil {
return err
}
qr := QuerierFunc(func(ctx context.Context, q Query) (Value, error) {
return query.sqlAll(ctx, func(_ context.Context, spec *sqlgraph.QuerySpec) {
assign := spec.Assign
values := spec.ScanValues
spec.ScanValues = func(columns []string) ([]any, error) {
values, err := values(columns[1:])
if err != nil {
return nil, err
}
return append([]any{new(uuid.UUID)}, values...), nil
}
spec.Assign = func(columns []string, values []any) error {
outValue := *values[0].(*uuid.UUID)
inValue := *values[1].(*uuid.UUID)
if nids[inValue] == nil {
nids[inValue] = map[*Organization]struct{}{byID[outValue]: {}}
return assign(columns[1:], values[1:])
}
nids[inValue][byID[outValue]] = struct{}{}
return nil
}
})
})
neighbors, err := withInterceptors[[]*User](ctx, query, qr, query.inters)
if err != nil {
return err
}
for _, n := range neighbors {
nodes, ok := nids[n.ID]
if !ok {
return fmt.Errorf(`unexpected "users" node returned %v`, n.ID)
}
for kn := range nodes {
assign(kn, n)
}
}
return nil
}
func (oq *OrganizationQuery) sqlCount(ctx context.Context) (int, error) {
_spec := oq.querySpec()
_spec.Node.Columns = oq.ctx.Fields
@@ -380,6 +561,9 @@ func (oq *OrganizationQuery) querySpec() *sqlgraph.QuerySpec {
_spec.Node.Columns = append(_spec.Node.Columns, fields[i])
}
}
if oq.withCreator != nil {
_spec.Node.AddColumnOnce(organization.FieldCreatorID)
}
}
if ps := oq.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {

View File

@@ -11,8 +11,10 @@ import (
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/gofrs/uuid"
"github.com/holos-run/holos/internal/ent/organization"
"github.com/holos-run/holos/internal/ent/predicate"
"github.com/holos-run/holos/internal/ent/user"
)
// OrganizationUpdate is the builder for updating Organization entities.
@@ -62,11 +64,72 @@ func (ou *OrganizationUpdate) SetNillableDisplayName(s *string) *OrganizationUpd
return ou
}
// SetCreatorID sets the "creator_id" field.
func (ou *OrganizationUpdate) SetCreatorID(u uuid.UUID) *OrganizationUpdate {
ou.mutation.SetCreatorID(u)
return ou
}
// SetNillableCreatorID sets the "creator_id" field if the given value is not nil.
func (ou *OrganizationUpdate) SetNillableCreatorID(u *uuid.UUID) *OrganizationUpdate {
if u != nil {
ou.SetCreatorID(*u)
}
return ou
}
// SetCreator sets the "creator" edge to the User entity.
func (ou *OrganizationUpdate) SetCreator(u *User) *OrganizationUpdate {
return ou.SetCreatorID(u.ID)
}
// AddUserIDs adds the "users" edge to the User entity by IDs.
func (ou *OrganizationUpdate) AddUserIDs(ids ...uuid.UUID) *OrganizationUpdate {
ou.mutation.AddUserIDs(ids...)
return ou
}
// AddUsers adds the "users" edges to the User entity.
func (ou *OrganizationUpdate) AddUsers(u ...*User) *OrganizationUpdate {
ids := make([]uuid.UUID, len(u))
for i := range u {
ids[i] = u[i].ID
}
return ou.AddUserIDs(ids...)
}
// Mutation returns the OrganizationMutation object of the builder.
func (ou *OrganizationUpdate) Mutation() *OrganizationMutation {
return ou.mutation
}
// ClearCreator clears the "creator" edge to the User entity.
func (ou *OrganizationUpdate) ClearCreator() *OrganizationUpdate {
ou.mutation.ClearCreator()
return ou
}
// ClearUsers clears all "users" edges to the User entity.
func (ou *OrganizationUpdate) ClearUsers() *OrganizationUpdate {
ou.mutation.ClearUsers()
return ou
}
// RemoveUserIDs removes the "users" edge to User entities by IDs.
func (ou *OrganizationUpdate) RemoveUserIDs(ids ...uuid.UUID) *OrganizationUpdate {
ou.mutation.RemoveUserIDs(ids...)
return ou
}
// RemoveUsers removes "users" edges to User entities.
func (ou *OrganizationUpdate) RemoveUsers(u ...*User) *OrganizationUpdate {
ids := make([]uuid.UUID, len(u))
for i := range u {
ids[i] = u[i].ID
}
return ou.RemoveUserIDs(ids...)
}
// Save executes the query and returns the number of nodes affected by the update operation.
func (ou *OrganizationUpdate) Save(ctx context.Context) (int, error) {
ou.defaults()
@@ -110,6 +173,9 @@ func (ou *OrganizationUpdate) check() error {
return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Organization.name": %w`, err)}
}
}
if _, ok := ou.mutation.CreatorID(); ou.mutation.CreatorCleared() && !ok {
return errors.New(`ent: clearing a required unique edge "Organization.creator"`)
}
return nil
}
@@ -134,6 +200,80 @@ func (ou *OrganizationUpdate) sqlSave(ctx context.Context) (n int, err error) {
if value, ok := ou.mutation.DisplayName(); ok {
_spec.SetField(organization.FieldDisplayName, field.TypeString, value)
}
if ou.mutation.CreatorCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: false,
Table: organization.CreatorTable,
Columns: []string{organization.CreatorColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := ou.mutation.CreatorIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: false,
Table: organization.CreatorTable,
Columns: []string{organization.CreatorColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if ou.mutation.UsersCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: false,
Table: organization.UsersTable,
Columns: organization.UsersPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := ou.mutation.RemovedUsersIDs(); len(nodes) > 0 && !ou.mutation.UsersCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: false,
Table: organization.UsersTable,
Columns: organization.UsersPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := ou.mutation.UsersIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: false,
Table: organization.UsersTable,
Columns: organization.UsersPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if n, err = sqlgraph.UpdateNodes(ctx, ou.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{organization.Label}
@@ -188,11 +328,72 @@ func (ouo *OrganizationUpdateOne) SetNillableDisplayName(s *string) *Organizatio
return ouo
}
// SetCreatorID sets the "creator_id" field.
func (ouo *OrganizationUpdateOne) SetCreatorID(u uuid.UUID) *OrganizationUpdateOne {
ouo.mutation.SetCreatorID(u)
return ouo
}
// SetNillableCreatorID sets the "creator_id" field if the given value is not nil.
func (ouo *OrganizationUpdateOne) SetNillableCreatorID(u *uuid.UUID) *OrganizationUpdateOne {
if u != nil {
ouo.SetCreatorID(*u)
}
return ouo
}
// SetCreator sets the "creator" edge to the User entity.
func (ouo *OrganizationUpdateOne) SetCreator(u *User) *OrganizationUpdateOne {
return ouo.SetCreatorID(u.ID)
}
// AddUserIDs adds the "users" edge to the User entity by IDs.
func (ouo *OrganizationUpdateOne) AddUserIDs(ids ...uuid.UUID) *OrganizationUpdateOne {
ouo.mutation.AddUserIDs(ids...)
return ouo
}
// AddUsers adds the "users" edges to the User entity.
func (ouo *OrganizationUpdateOne) AddUsers(u ...*User) *OrganizationUpdateOne {
ids := make([]uuid.UUID, len(u))
for i := range u {
ids[i] = u[i].ID
}
return ouo.AddUserIDs(ids...)
}
// Mutation returns the OrganizationMutation object of the builder.
func (ouo *OrganizationUpdateOne) Mutation() *OrganizationMutation {
return ouo.mutation
}
// ClearCreator clears the "creator" edge to the User entity.
func (ouo *OrganizationUpdateOne) ClearCreator() *OrganizationUpdateOne {
ouo.mutation.ClearCreator()
return ouo
}
// ClearUsers clears all "users" edges to the User entity.
func (ouo *OrganizationUpdateOne) ClearUsers() *OrganizationUpdateOne {
ouo.mutation.ClearUsers()
return ouo
}
// RemoveUserIDs removes the "users" edge to User entities by IDs.
func (ouo *OrganizationUpdateOne) RemoveUserIDs(ids ...uuid.UUID) *OrganizationUpdateOne {
ouo.mutation.RemoveUserIDs(ids...)
return ouo
}
// RemoveUsers removes "users" edges to User entities.
func (ouo *OrganizationUpdateOne) RemoveUsers(u ...*User) *OrganizationUpdateOne {
ids := make([]uuid.UUID, len(u))
for i := range u {
ids[i] = u[i].ID
}
return ouo.RemoveUserIDs(ids...)
}
// Where appends a list predicates to the OrganizationUpdate builder.
func (ouo *OrganizationUpdateOne) Where(ps ...predicate.Organization) *OrganizationUpdateOne {
ouo.mutation.Where(ps...)
@@ -249,6 +450,9 @@ func (ouo *OrganizationUpdateOne) check() error {
return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Organization.name": %w`, err)}
}
}
if _, ok := ouo.mutation.CreatorID(); ouo.mutation.CreatorCleared() && !ok {
return errors.New(`ent: clearing a required unique edge "Organization.creator"`)
}
return nil
}
@@ -290,6 +494,80 @@ func (ouo *OrganizationUpdateOne) sqlSave(ctx context.Context) (_node *Organizat
if value, ok := ouo.mutation.DisplayName(); ok {
_spec.SetField(organization.FieldDisplayName, field.TypeString, value)
}
if ouo.mutation.CreatorCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: false,
Table: organization.CreatorTable,
Columns: []string{organization.CreatorColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := ouo.mutation.CreatorIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: false,
Table: organization.CreatorTable,
Columns: []string{organization.CreatorColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if ouo.mutation.UsersCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: false,
Table: organization.UsersTable,
Columns: organization.UsersPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := ouo.mutation.RemovedUsersIDs(); len(nodes) > 0 && !ouo.mutation.UsersCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: false,
Table: organization.UsersTable,
Columns: organization.UsersPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := ouo.mutation.UsersIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: false,
Table: organization.UsersTable,
Columns: organization.UsersPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
_node = &Organization{config: ouo.config}
_spec.Assign = _node.assignValues
_spec.ScanValues = _node.scanValues

View File

@@ -1,11 +1,12 @@
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/mixin"
"github.com/gofrs/uuid"
"time"
)
func newUUID() uuid.UUID {
@@ -18,6 +19,7 @@ type BaseMixin struct {
func (BaseMixin) Fields() []ent.Field {
return []ent.Field{
// id represents the identity of the entity.
field.UUID("id", uuid.UUID{}).Default(newUUID),
}
}

View File

@@ -2,26 +2,37 @@ package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"github.com/gofrs/uuid"
)
// User holds the schema definition for the User entity, the internal
// representation and identity of a single human user. Users are scoped
// globally.
// Organization represents an organization account.
type Organization struct {
ent.Schema
}
func (Organization) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty().Unique(),
field.String("display_name"),
}
}
func (Organization) Mixin() []ent.Mixin {
return []ent.Mixin{
BaseMixin{},
TimeMixin{},
}
}
func (Organization) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty().Unique(),
field.String("display_name"),
field.UUID("creator_id", uuid.UUID{}),
}
}
func (Organization) Edges() []ent.Edge {
return []ent.Edge{
edge.To("creator", User.Type).
Field("creator_id").
Unique().
Required(),
edge.To("users", User.Type),
}
}

View File

@@ -2,7 +2,9 @@ package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)
// User holds the schema definition for the User entity, the internal
@@ -28,3 +30,16 @@ func (User) Fields() []ent.Field {
field.String("name"),
}
}
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.From("organizations", Organization.Type).
Ref("users"),
}
}
func (User) Indexes() []ent.Index {
return []ent.Index{
index.Fields("iss", "sub").Unique(),
}
}

View File

@@ -29,10 +29,31 @@ type User struct {
// Sub holds the value of the "sub" field.
Sub string `json:"sub,omitempty"`
// Name holds the value of the "name" field.
Name string `json:"name,omitempty"`
Name string `json:"name,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the UserQuery when eager-loading is set.
Edges UserEdges `json:"edges"`
selectValues sql.SelectValues
}
// UserEdges holds the relations/edges for other nodes in the graph.
type UserEdges struct {
// Organizations holds the value of the organizations edge.
Organizations []*Organization `json:"organizations,omitempty"`
// loadedTypes holds the information for reporting if a
// type was loaded (or requested) in eager-loading or not.
loadedTypes [1]bool
}
// OrganizationsOrErr returns the Organizations value or an error if the edge
// was not loaded in eager-loading.
func (e UserEdges) OrganizationsOrErr() ([]*Organization, error) {
if e.loadedTypes[0] {
return e.Organizations, nil
}
return nil, &NotLoadedError{edge: "organizations"}
}
// scanValues returns the types for scanning values from sql.Rows.
func (*User) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
@@ -114,6 +135,11 @@ func (u *User) Value(name string) (ent.Value, error) {
return u.selectValues.Get(name)
}
// QueryOrganizations queries the "organizations" edge of the User entity.
func (u *User) QueryOrganizations() *OrganizationQuery {
return NewUserClient(u.config).QueryOrganizations(u)
}
// Update returns a builder for updating this User.
// Note that you need to call User.Unwrap() before calling this method if this User
// was returned from a transaction, and the transaction was committed or rolled back.

View File

@@ -6,6 +6,7 @@ import (
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/gofrs/uuid"
)
@@ -26,8 +27,15 @@ const (
FieldSub = "sub"
// FieldName holds the string denoting the name field in the database.
FieldName = "name"
// EdgeOrganizations holds the string denoting the organizations edge name in mutations.
EdgeOrganizations = "organizations"
// Table holds the table name of the user in the database.
Table = "users"
// OrganizationsTable is the table that holds the organizations relation/edge. The primary key declared below.
OrganizationsTable = "organization_users"
// OrganizationsInverseTable is the table name for the Organization entity.
// It exists in this package in order to avoid circular dependency with the "organization" package.
OrganizationsInverseTable = "organizations"
)
// Columns holds all SQL columns for user fields.
@@ -41,6 +49,12 @@ var Columns = []string{
FieldName,
}
var (
// OrganizationsPrimaryKey and OrganizationsColumn2 are the table columns denoting the
// primary key for the organizations relation (M2M).
OrganizationsPrimaryKey = []string{"organization_id", "user_id"}
)
// ValidColumn reports if the column name is valid (part of the table columns).
func ValidColumn(column string) bool {
for i := range Columns {
@@ -101,3 +115,24 @@ func BySub(opts ...sql.OrderTermOption) OrderOption {
func ByName(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldName, opts...).ToFunc()
}
// ByOrganizationsCount orders the results by organizations count.
func ByOrganizationsCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborsCount(s, newOrganizationsStep(), opts...)
}
}
// ByOrganizations orders the results by organizations terms.
func ByOrganizations(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newOrganizationsStep(), append([]sql.OrderTerm{term}, terms...)...)
}
}
func newOrganizationsStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(OrganizationsInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.M2M, true, OrganizationsTable, OrganizationsPrimaryKey...),
)
}

View File

@@ -6,6 +6,7 @@ import (
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/gofrs/uuid"
"github.com/holos-run/holos/internal/ent/predicate"
)
@@ -425,6 +426,29 @@ func NameContainsFold(v string) predicate.User {
return predicate.User(sql.FieldContainsFold(FieldName, v))
}
// HasOrganizations applies the HasEdge predicate on the "organizations" edge.
func HasOrganizations() predicate.User {
return predicate.User(func(s *sql.Selector) {
step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.Edge(sqlgraph.M2M, true, OrganizationsTable, OrganizationsPrimaryKey...),
)
sqlgraph.HasNeighbors(s, step)
})
}
// HasOrganizationsWith applies the HasEdge predicate on the "organizations" edge with a given conditions (other predicates).
func HasOrganizationsWith(preds ...predicate.Organization) predicate.User {
return predicate.User(func(s *sql.Selector) {
step := newOrganizationsStep()
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds {
p(s)
}
})
})
}
// And groups predicates with the AND operator between them.
func And(predicates ...predicate.User) predicate.User {
return predicate.User(sql.AndPredicates(predicates...))

View File

@@ -13,6 +13,7 @@ import (
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/gofrs/uuid"
"github.com/holos-run/holos/internal/ent/organization"
"github.com/holos-run/holos/internal/ent/user"
)
@@ -90,6 +91,21 @@ func (uc *UserCreate) SetNillableID(u *uuid.UUID) *UserCreate {
return uc
}
// AddOrganizationIDs adds the "organizations" edge to the Organization entity by IDs.
func (uc *UserCreate) AddOrganizationIDs(ids ...uuid.UUID) *UserCreate {
uc.mutation.AddOrganizationIDs(ids...)
return uc
}
// AddOrganizations adds the "organizations" edges to the Organization entity.
func (uc *UserCreate) AddOrganizations(o ...*Organization) *UserCreate {
ids := make([]uuid.UUID, len(o))
for i := range o {
ids[i] = o[i].ID
}
return uc.AddOrganizationIDs(ids...)
}
// Mutation returns the UserMutation object of the builder.
func (uc *UserCreate) Mutation() *UserMutation {
return uc.mutation
@@ -224,6 +240,22 @@ func (uc *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) {
_spec.SetField(user.FieldName, field.TypeString, value)
_node.Name = value
}
if nodes := uc.mutation.OrganizationsIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: true,
Table: user.OrganizationsTable,
Columns: user.OrganizationsPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(organization.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges = append(_spec.Edges, edge)
}
return _node, _spec
}

View File

@@ -4,6 +4,7 @@ package ent
import (
"context"
"database/sql/driver"
"fmt"
"math"
@@ -11,6 +12,7 @@ import (
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/gofrs/uuid"
"github.com/holos-run/holos/internal/ent/organization"
"github.com/holos-run/holos/internal/ent/predicate"
"github.com/holos-run/holos/internal/ent/user"
)
@@ -18,10 +20,11 @@ import (
// UserQuery is the builder for querying User entities.
type UserQuery struct {
config
ctx *QueryContext
order []user.OrderOption
inters []Interceptor
predicates []predicate.User
ctx *QueryContext
order []user.OrderOption
inters []Interceptor
predicates []predicate.User
withOrganizations *OrganizationQuery
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -58,6 +61,28 @@ func (uq *UserQuery) Order(o ...user.OrderOption) *UserQuery {
return uq
}
// QueryOrganizations chains the current query on the "organizations" edge.
func (uq *UserQuery) QueryOrganizations() *OrganizationQuery {
query := (&OrganizationClient{config: uq.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
if err := uq.prepareQuery(ctx); err != nil {
return nil, err
}
selector := uq.sqlQuery(ctx)
if err := selector.Err(); err != nil {
return nil, err
}
step := sqlgraph.NewStep(
sqlgraph.From(user.Table, user.FieldID, selector),
sqlgraph.To(organization.Table, organization.FieldID),
sqlgraph.Edge(sqlgraph.M2M, true, user.OrganizationsTable, user.OrganizationsPrimaryKey...),
)
fromU = sqlgraph.SetNeighbors(uq.driver.Dialect(), step)
return fromU, nil
}
return query
}
// First returns the first User entity from the query.
// Returns a *NotFoundError when no User was found.
func (uq *UserQuery) First(ctx context.Context) (*User, error) {
@@ -245,17 +270,29 @@ func (uq *UserQuery) Clone() *UserQuery {
return nil
}
return &UserQuery{
config: uq.config,
ctx: uq.ctx.Clone(),
order: append([]user.OrderOption{}, uq.order...),
inters: append([]Interceptor{}, uq.inters...),
predicates: append([]predicate.User{}, uq.predicates...),
config: uq.config,
ctx: uq.ctx.Clone(),
order: append([]user.OrderOption{}, uq.order...),
inters: append([]Interceptor{}, uq.inters...),
predicates: append([]predicate.User{}, uq.predicates...),
withOrganizations: uq.withOrganizations.Clone(),
// clone intermediate query.
sql: uq.sql.Clone(),
path: uq.path,
}
}
// WithOrganizations tells the query-builder to eager-load the nodes that are connected to
// the "organizations" edge. The optional arguments are used to configure the query builder of the edge.
func (uq *UserQuery) WithOrganizations(opts ...func(*OrganizationQuery)) *UserQuery {
query := (&OrganizationClient{config: uq.config}).Query()
for _, opt := range opts {
opt(query)
}
uq.withOrganizations = query
return uq
}
// GroupBy is used to group vertices by one or more fields/columns.
// It is often used with aggregate functions, like: count, max, mean, min, sum.
//
@@ -332,8 +369,11 @@ func (uq *UserQuery) prepareQuery(ctx context.Context) error {
func (uq *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, error) {
var (
nodes = []*User{}
_spec = uq.querySpec()
nodes = []*User{}
_spec = uq.querySpec()
loadedTypes = [1]bool{
uq.withOrganizations != nil,
}
)
_spec.ScanValues = func(columns []string) ([]any, error) {
return (*User).scanValues(nil, columns)
@@ -341,6 +381,7 @@ func (uq *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e
_spec.Assign = func(columns []string, values []any) error {
node := &User{config: uq.config}
nodes = append(nodes, node)
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
for i := range hooks {
@@ -352,9 +393,78 @@ func (uq *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e
if len(nodes) == 0 {
return nodes, nil
}
if query := uq.withOrganizations; query != nil {
if err := uq.loadOrganizations(ctx, query, nodes,
func(n *User) { n.Edges.Organizations = []*Organization{} },
func(n *User, e *Organization) { n.Edges.Organizations = append(n.Edges.Organizations, e) }); err != nil {
return nil, err
}
}
return nodes, nil
}
func (uq *UserQuery) loadOrganizations(ctx context.Context, query *OrganizationQuery, nodes []*User, init func(*User), assign func(*User, *Organization)) error {
edgeIDs := make([]driver.Value, len(nodes))
byID := make(map[uuid.UUID]*User)
nids := make(map[uuid.UUID]map[*User]struct{})
for i, node := range nodes {
edgeIDs[i] = node.ID
byID[node.ID] = node
if init != nil {
init(node)
}
}
query.Where(func(s *sql.Selector) {
joinT := sql.Table(user.OrganizationsTable)
s.Join(joinT).On(s.C(organization.FieldID), joinT.C(user.OrganizationsPrimaryKey[0]))
s.Where(sql.InValues(joinT.C(user.OrganizationsPrimaryKey[1]), edgeIDs...))
columns := s.SelectedColumns()
s.Select(joinT.C(user.OrganizationsPrimaryKey[1]))
s.AppendSelect(columns...)
s.SetDistinct(false)
})
if err := query.prepareQuery(ctx); err != nil {
return err
}
qr := QuerierFunc(func(ctx context.Context, q Query) (Value, error) {
return query.sqlAll(ctx, func(_ context.Context, spec *sqlgraph.QuerySpec) {
assign := spec.Assign
values := spec.ScanValues
spec.ScanValues = func(columns []string) ([]any, error) {
values, err := values(columns[1:])
if err != nil {
return nil, err
}
return append([]any{new(uuid.UUID)}, values...), nil
}
spec.Assign = func(columns []string, values []any) error {
outValue := *values[0].(*uuid.UUID)
inValue := *values[1].(*uuid.UUID)
if nids[inValue] == nil {
nids[inValue] = map[*User]struct{}{byID[outValue]: {}}
return assign(columns[1:], values[1:])
}
nids[inValue][byID[outValue]] = struct{}{}
return nil
}
})
})
neighbors, err := withInterceptors[[]*Organization](ctx, query, qr, query.inters)
if err != nil {
return err
}
for _, n := range neighbors {
nodes, ok := nids[n.ID]
if !ok {
return fmt.Errorf(`unexpected "organizations" node returned %v`, n.ID)
}
for kn := range nodes {
assign(kn, n)
}
}
return nil
}
func (uq *UserQuery) sqlCount(ctx context.Context) (int, error) {
_spec := uq.querySpec()
_spec.Node.Columns = uq.ctx.Fields

View File

@@ -11,6 +11,8 @@ import (
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/gofrs/uuid"
"github.com/holos-run/holos/internal/ent/organization"
"github.com/holos-run/holos/internal/ent/predicate"
"github.com/holos-run/holos/internal/ent/user"
)
@@ -90,11 +92,47 @@ func (uu *UserUpdate) SetNillableName(s *string) *UserUpdate {
return uu
}
// AddOrganizationIDs adds the "organizations" edge to the Organization entity by IDs.
func (uu *UserUpdate) AddOrganizationIDs(ids ...uuid.UUID) *UserUpdate {
uu.mutation.AddOrganizationIDs(ids...)
return uu
}
// AddOrganizations adds the "organizations" edges to the Organization entity.
func (uu *UserUpdate) AddOrganizations(o ...*Organization) *UserUpdate {
ids := make([]uuid.UUID, len(o))
for i := range o {
ids[i] = o[i].ID
}
return uu.AddOrganizationIDs(ids...)
}
// Mutation returns the UserMutation object of the builder.
func (uu *UserUpdate) Mutation() *UserMutation {
return uu.mutation
}
// ClearOrganizations clears all "organizations" edges to the Organization entity.
func (uu *UserUpdate) ClearOrganizations() *UserUpdate {
uu.mutation.ClearOrganizations()
return uu
}
// RemoveOrganizationIDs removes the "organizations" edge to Organization entities by IDs.
func (uu *UserUpdate) RemoveOrganizationIDs(ids ...uuid.UUID) *UserUpdate {
uu.mutation.RemoveOrganizationIDs(ids...)
return uu
}
// RemoveOrganizations removes "organizations" edges to Organization entities.
func (uu *UserUpdate) RemoveOrganizations(o ...*Organization) *UserUpdate {
ids := make([]uuid.UUID, len(o))
for i := range o {
ids[i] = o[i].ID
}
return uu.RemoveOrganizationIDs(ids...)
}
// Save executes the query and returns the number of nodes affected by the update operation.
func (uu *UserUpdate) Save(ctx context.Context) (int, error) {
uu.defaults()
@@ -168,6 +206,51 @@ func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) {
if value, ok := uu.mutation.Name(); ok {
_spec.SetField(user.FieldName, field.TypeString, value)
}
if uu.mutation.OrganizationsCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: true,
Table: user.OrganizationsTable,
Columns: user.OrganizationsPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(organization.FieldID, field.TypeUUID),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := uu.mutation.RemovedOrganizationsIDs(); len(nodes) > 0 && !uu.mutation.OrganizationsCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: true,
Table: user.OrganizationsTable,
Columns: user.OrganizationsPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(organization.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := uu.mutation.OrganizationsIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: true,
Table: user.OrganizationsTable,
Columns: user.OrganizationsPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(organization.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if n, err = sqlgraph.UpdateNodes(ctx, uu.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{user.Label}
@@ -250,11 +333,47 @@ func (uuo *UserUpdateOne) SetNillableName(s *string) *UserUpdateOne {
return uuo
}
// AddOrganizationIDs adds the "organizations" edge to the Organization entity by IDs.
func (uuo *UserUpdateOne) AddOrganizationIDs(ids ...uuid.UUID) *UserUpdateOne {
uuo.mutation.AddOrganizationIDs(ids...)
return uuo
}
// AddOrganizations adds the "organizations" edges to the Organization entity.
func (uuo *UserUpdateOne) AddOrganizations(o ...*Organization) *UserUpdateOne {
ids := make([]uuid.UUID, len(o))
for i := range o {
ids[i] = o[i].ID
}
return uuo.AddOrganizationIDs(ids...)
}
// Mutation returns the UserMutation object of the builder.
func (uuo *UserUpdateOne) Mutation() *UserMutation {
return uuo.mutation
}
// ClearOrganizations clears all "organizations" edges to the Organization entity.
func (uuo *UserUpdateOne) ClearOrganizations() *UserUpdateOne {
uuo.mutation.ClearOrganizations()
return uuo
}
// RemoveOrganizationIDs removes the "organizations" edge to Organization entities by IDs.
func (uuo *UserUpdateOne) RemoveOrganizationIDs(ids ...uuid.UUID) *UserUpdateOne {
uuo.mutation.RemoveOrganizationIDs(ids...)
return uuo
}
// RemoveOrganizations removes "organizations" edges to Organization entities.
func (uuo *UserUpdateOne) RemoveOrganizations(o ...*Organization) *UserUpdateOne {
ids := make([]uuid.UUID, len(o))
for i := range o {
ids[i] = o[i].ID
}
return uuo.RemoveOrganizationIDs(ids...)
}
// Where appends a list predicates to the UserUpdate builder.
func (uuo *UserUpdateOne) Where(ps ...predicate.User) *UserUpdateOne {
uuo.mutation.Where(ps...)
@@ -358,6 +477,51 @@ func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error)
if value, ok := uuo.mutation.Name(); ok {
_spec.SetField(user.FieldName, field.TypeString, value)
}
if uuo.mutation.OrganizationsCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: true,
Table: user.OrganizationsTable,
Columns: user.OrganizationsPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(organization.FieldID, field.TypeUUID),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := uuo.mutation.RemovedOrganizationsIDs(); len(nodes) > 0 && !uuo.mutation.OrganizationsCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: true,
Table: user.OrganizationsTable,
Columns: user.OrganizationsPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(organization.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := uuo.mutation.OrganizationsIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: true,
Table: user.OrganizationsTable,
Columns: user.OrganizationsPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(organization.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
_node = &User{config: uuo.config}
_spec.Assign = _node.assignValues
_spec.ScanValues = _node.scanValues

View File

@@ -1,35 +0,0 @@
// @generated by protoc-gen-connect-query v1.3.1 with parameter "target=ts"
// @generated from file holos/v1alpha1/holos.proto (package holos.v1alpha1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import { MethodKind } from "@bufbuild/protobuf";
import { GetUserClaimsRequest, GetUserClaimsResponse, RegisterUserRequest, RegisterUserResponse } from "./holos_pb.js";
/**
* @generated from rpc holos.v1alpha1.HolosService.GetUserClaims
*/
export const getUserClaims = {
localName: "getUserClaims",
name: "GetUserClaims",
kind: MethodKind.Unary,
I: GetUserClaimsRequest,
O: GetUserClaimsResponse,
service: {
typeName: "holos.v1alpha1.HolosService"
}
} as const;
/**
* @generated from rpc holos.v1alpha1.HolosService.RegisterUser
*/
export const registerUser = {
localName: "registerUser",
name: "RegisterUser",
kind: MethodKind.Unary,
I: RegisterUserRequest,
O: RegisterUserResponse,
service: {
typeName: "holos.v1alpha1.HolosService"
}
} as const;

View File

@@ -1,35 +0,0 @@
// @generated by protoc-gen-connect-es v1.4.0 with parameter "target=ts"
// @generated from file holos/v1alpha1/holos.proto (package holos.v1alpha1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import { GetUserClaimsRequest, GetUserClaimsResponse, RegisterUserRequest, RegisterUserResponse } from "./holos_pb.js";
import { MethodKind } from "@bufbuild/protobuf";
/**
* @generated from service holos.v1alpha1.HolosService
*/
export const HolosService = {
typeName: "holos.v1alpha1.HolosService",
methods: {
/**
* @generated from rpc holos.v1alpha1.HolosService.GetUserClaims
*/
getUserClaims: {
name: "GetUserClaims",
I: GetUserClaimsRequest,
O: GetUserClaimsResponse,
kind: MethodKind.Unary,
},
/**
* @generated from rpc holos.v1alpha1.HolosService.RegisterUser
*/
registerUser: {
name: "RegisterUser",
I: RegisterUserRequest,
O: RegisterUserResponse,
kind: MethodKind.Unary,
},
}
} as const;

View File

@@ -1,296 +0,0 @@
// @generated by protoc-gen-es v1.8.0 with parameter "target=ts"
// @generated from file holos/v1alpha1/holos.proto (package holos.v1alpha1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import { Message, proto3, Timestamp } from "@bufbuild/protobuf";
/**
* @generated from message holos.v1alpha1.Timestamps
*/
export class Timestamps extends Message<Timestamps> {
/**
* Created at timestamp
*
* @generated from field: google.protobuf.Timestamp created_at = 1;
*/
createdAt?: Timestamp;
/**
* Updated at timestamp
*
* @generated from field: google.protobuf.Timestamp updated_at = 2;
*/
updatedAt?: Timestamp;
constructor(data?: PartialMessage<Timestamps>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.Timestamps";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "created_at", kind: "message", T: Timestamp },
{ no: 2, name: "updated_at", kind: "message", T: Timestamp },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Timestamps {
return new Timestamps().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Timestamps {
return new Timestamps().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Timestamps {
return new Timestamps().fromJsonString(jsonString, options);
}
static equals(a: Timestamps | PlainMessage<Timestamps> | undefined, b: Timestamps | PlainMessage<Timestamps> | undefined): boolean {
return proto3.util.equals(Timestamps, a, b);
}
}
/**
* Empty request, claims are pulled from the id token
*
* @generated from message holos.v1alpha1.GetUserClaimsRequest
*/
export class GetUserClaimsRequest extends Message<GetUserClaimsRequest> {
constructor(data?: PartialMessage<GetUserClaimsRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.GetUserClaimsRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetUserClaimsRequest {
return new GetUserClaimsRequest().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetUserClaimsRequest {
return new GetUserClaimsRequest().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetUserClaimsRequest {
return new GetUserClaimsRequest().fromJsonString(jsonString, options);
}
static equals(a: GetUserClaimsRequest | PlainMessage<GetUserClaimsRequest> | undefined, b: GetUserClaimsRequest | PlainMessage<GetUserClaimsRequest> | undefined): boolean {
return proto3.util.equals(GetUserClaimsRequest, a, b);
}
}
/**
* UserClaims represents id token claims
*
* @generated from message holos.v1alpha1.GetUserClaimsResponse
*/
export class GetUserClaimsResponse extends Message<GetUserClaimsResponse> {
/**
* @generated from field: string iss = 1;
*/
iss = "";
/**
* @generated from field: string sub = 2;
*/
sub = "";
/**
* @generated from field: string email = 3;
*/
email = "";
/**
* @generated from field: bool email_verified = 4;
*/
emailVerified = false;
/**
* @generated from field: string name = 5;
*/
name = "";
constructor(data?: PartialMessage<GetUserClaimsResponse>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.GetUserClaimsResponse";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "iss", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "sub", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "email", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 4, name: "email_verified", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
{ no: 5, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetUserClaimsResponse {
return new GetUserClaimsResponse().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetUserClaimsResponse {
return new GetUserClaimsResponse().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetUserClaimsResponse {
return new GetUserClaimsResponse().fromJsonString(jsonString, options);
}
static equals(a: GetUserClaimsResponse | PlainMessage<GetUserClaimsResponse> | undefined, b: GetUserClaimsResponse | PlainMessage<GetUserClaimsResponse> | undefined): boolean {
return proto3.util.equals(GetUserClaimsResponse, a, b);
}
}
/**
* User represents a human user in the system. See db schema in ent/schema/user.go
*
* @generated from message holos.v1alpha1.User
*/
export class User extends Message<User> {
/**
* Unique id assigned by the server.
*
* @generated from field: string id = 1;
*/
id = "";
/**
* @generated from field: string email = 2;
*/
email = "";
/**
* @generated from field: bool email_verified = 3;
*/
emailVerified = false;
/**
* @generated from field: string name = 4;
*/
name = "";
/**
* @generated from field: holos.v1alpha1.Timestamps timestamps = 5;
*/
timestamps?: Timestamps;
constructor(data?: PartialMessage<User>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.User";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "email", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "email_verified", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
{ no: 4, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 5, name: "timestamps", kind: "message", T: Timestamps },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): User {
return new User().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): User {
return new User().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): User {
return new User().fromJsonString(jsonString, options);
}
static equals(a: User | PlainMessage<User> | undefined, b: User | PlainMessage<User> | undefined): boolean {
return proto3.util.equals(User, a, b);
}
}
/**
* @generated from message holos.v1alpha1.RegisterUserRequest
*/
export class RegisterUserRequest extends Message<RegisterUserRequest> {
/**
* @generated from field: optional string name = 1;
*/
name?: string;
constructor(data?: PartialMessage<RegisterUserRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.RegisterUserRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RegisterUserRequest {
return new RegisterUserRequest().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): RegisterUserRequest {
return new RegisterUserRequest().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): RegisterUserRequest {
return new RegisterUserRequest().fromJsonString(jsonString, options);
}
static equals(a: RegisterUserRequest | PlainMessage<RegisterUserRequest> | undefined, b: RegisterUserRequest | PlainMessage<RegisterUserRequest> | undefined): boolean {
return proto3.util.equals(RegisterUserRequest, a, b);
}
}
/**
* @generated from message holos.v1alpha1.RegisterUserResponse
*/
export class RegisterUserResponse extends Message<RegisterUserResponse> {
/**
* @generated from field: holos.v1alpha1.User user = 1;
*/
user?: User;
/**
* @generated from field: bool already_exists = 2;
*/
alreadyExists = false;
constructor(data?: PartialMessage<RegisterUserResponse>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.RegisterUserResponse";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "user", kind: "message", T: User },
{ no: 2, name: "already_exists", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RegisterUserResponse {
return new RegisterUserResponse().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): RegisterUserResponse {
return new RegisterUserResponse().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): RegisterUserResponse {
return new RegisterUserResponse().fromJsonString(jsonString, options);
}
static equals(a: RegisterUserResponse | PlainMessage<RegisterUserResponse> | undefined, b: RegisterUserResponse | PlainMessage<RegisterUserResponse> | undefined): boolean {
return proto3.util.equals(RegisterUserResponse, a, b);
}
}

View File

@@ -18,7 +18,7 @@
"@angular/platform-browser": "^17.3.0",
"@angular/platform-browser-dynamic": "^17.3.0",
"@angular/router": "^17.3.0",
"@bufbuild/protobuf": "^1.8.0",
"@bufbuild/protobuf": "^1.9.0",
"@connectrpc/connect": "^1.4.0",
"@connectrpc/connect-query": "^1.3.1",
"@connectrpc/connect-web": "^1.4.0",
@@ -30,8 +30,8 @@
"@angular-devkit/build-angular": "^17.3.4",
"@angular/cli": "^17.3.4",
"@angular/compiler-cli": "^17.3.0",
"@bufbuild/buf": "^1.30.1",
"@bufbuild/protoc-gen-es": "^1.8.0",
"@bufbuild/buf": "^1.31.0",
"@bufbuild/protoc-gen-es": "^1.9.0",
"@connectrpc/protoc-gen-connect-es": "^1.4.0",
"@connectrpc/protoc-gen-connect-query": "^1.3.1",
"@types/jasmine": "~5.1.0",
@@ -2337,9 +2337,9 @@
}
},
"node_modules/@bufbuild/buf": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.30.1.tgz",
"integrity": "sha512-9VVvrXBCWUiH8ToccqDfPRuTiPXSbHmSkL8XPlMpUhpJIlm01m4/Vzbc5FJL1yuk3e1rdBGCF6I9Obs9NsILzg==",
"version": "1.31.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.31.0.tgz",
"integrity": "sha512-kM/eueGkp0NDo8p8B6GXr1MdCzf4w8zEV1gbEiDlaLYDoyeHGLtlf5jF/hrb6MsvCccy3x7cc+cj4Wn/DmoR2g==",
"dev": true,
"hasInstallScript": true,
"bin": {
@@ -2351,18 +2351,18 @@
"node": ">=12"
},
"optionalDependencies": {
"@bufbuild/buf-darwin-arm64": "1.30.1",
"@bufbuild/buf-darwin-x64": "1.30.1",
"@bufbuild/buf-linux-aarch64": "1.30.1",
"@bufbuild/buf-linux-x64": "1.30.1",
"@bufbuild/buf-win32-arm64": "1.30.1",
"@bufbuild/buf-win32-x64": "1.30.1"
"@bufbuild/buf-darwin-arm64": "1.31.0",
"@bufbuild/buf-darwin-x64": "1.31.0",
"@bufbuild/buf-linux-aarch64": "1.31.0",
"@bufbuild/buf-linux-x64": "1.31.0",
"@bufbuild/buf-win32-arm64": "1.31.0",
"@bufbuild/buf-win32-x64": "1.31.0"
}
},
"node_modules/@bufbuild/buf-darwin-arm64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.30.1.tgz",
"integrity": "sha512-FRgf+x4V4s9Z1wH2xHdP8+1AYtil1GCmMjzKf/4AQ+eaUpoLfipSIsVYiBrnpcRxEPe9UMVzwNjKtPak/szwPw==",
"version": "1.31.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.31.0.tgz",
"integrity": "sha512-C0jArGS/SW0jfpBBmG6xEhUBWQTsGInnPr7y44WYWNS/U5OnnWPJtYQ7xbH0mzYDMx7sZVRV0FKvPO0FPKS+hA==",
"cpu": [
"arm64"
],
@@ -2376,9 +2376,9 @@
}
},
"node_modules/@bufbuild/buf-darwin-x64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.30.1.tgz",
"integrity": "sha512-kE0ne45zE7lSdv9WxPVhapwu627WMbWmWCzqSxzYr8sWDLqiAuw+XvO9/mHGdPWcMhV4lMX6tutitd9PPVxK8A==",
"version": "1.31.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.31.0.tgz",
"integrity": "sha512-oBTe1T4l2WSukAG+9YS7VHID4N1CuSvAxGBfuzpFQrjjiQZaaTfYuLqqVP6408MyCN7X/LOjfCekR1QToVweNw==",
"cpu": [
"x64"
],
@@ -2392,9 +2392,9 @@
}
},
"node_modules/@bufbuild/buf-linux-aarch64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.30.1.tgz",
"integrity": "sha512-kVV9Sl0GwZiQkMOXJiuwuU+gIHe6AWcYBMRMmuW55sY0ePZNXBmRGt4k5W4ijy98O6pnY3ao+n9ne0KwiD9MVA==",
"version": "1.31.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.31.0.tgz",
"integrity": "sha512-UN9KsTuO9YS5Vefj/CaqX1wO+hvc3AyGElxzOHMc7S3MWEuqSAFOhxu5I7CyOr2/yoZO2qZPPR29HuzmQsb2+w==",
"cpu": [
"arm64"
],
@@ -2408,9 +2408,9 @@
}
},
"node_modules/@bufbuild/buf-linux-x64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.30.1.tgz",
"integrity": "sha512-RacDbQJYNwqRlMESa/rLHprfUVa8Wu1/cmcqS29Fyt/cGzs0G8sNcQzQ87HYFIS9cSlSPl6vWL0x8JqQRp68lQ==",
"version": "1.31.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.31.0.tgz",
"integrity": "sha512-4uKq5Iu5tNEFE3Mz9a52KFCaywg5xLqwhN6Kf4kAk34kxWJgQ8D3WFe9ZpXHzH7Lj00u3Q+V/3vKCVATHR3tkw==",
"cpu": [
"x64"
],
@@ -2424,9 +2424,9 @@
}
},
"node_modules/@bufbuild/buf-win32-arm64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.30.1.tgz",
"integrity": "sha512-ndp/qb5M6yrSzcnMI0j4jjAuDKa7zHBFc187FwyDb3v63rvyQeYqncHb0leT5ZWqfNggJT4vXIH6QnH82PfDQw==",
"version": "1.31.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.31.0.tgz",
"integrity": "sha512-jz7GenlNsqwbC3qaHcBHBO35MycZ1gV8OUSRp/wTXGgZsEZzAyw335JA2NWL+5LaI8cF+CsYd6/uuWzKkdCKTQ==",
"cpu": [
"arm64"
],
@@ -2440,9 +2440,9 @@
}
},
"node_modules/@bufbuild/buf-win32-x64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.30.1.tgz",
"integrity": "sha512-1kmIY6oKLKZ4zIQVNG60GRDp+vKSZdaim7wRejOtgEDuWXhIuErlnGbpstypU8FO+OV3SeFUJNOJ8tLOYd3PvQ==",
"version": "1.31.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.31.0.tgz",
"integrity": "sha512-gw7p3PYam0g7hNwIhNphua5P8GBZczginoWNK3jk5sGVv0TzWOdHrjkVVhkc3DJbRZEg10ExGI3qRfCqiX1IHw==",
"cpu": [
"x64"
],
@@ -2456,18 +2456,18 @@
}
},
"node_modules/@bufbuild/protobuf": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.8.0.tgz",
"integrity": "sha512-qR9FwI8QKIveDnUYutvfzbC21UZJJryYrLuZGjeZ/VGz+vXelUkK+xgkOHsvPEdYEdxtgUUq4313N8QtOehJ1Q=="
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.9.0.tgz",
"integrity": "sha512-W7gp8Q/v1NlCZLsv8pQ3Y0uCu/SHgXOVFK+eUluUKWXmsb6VHkpNx0apdOWWcDbB9sJoKeP8uPrjmehJz6xETQ=="
},
"node_modules/@bufbuild/protoc-gen-es": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-es/-/protoc-gen-es-1.8.0.tgz",
"integrity": "sha512-jnvBKwHq3o/iOgfKxaxn5Za7ay4oAs8KWgoHiDc9Fsb0g+/d1z+mHlHvmevOiCPcVZsnH6V3LImOJvGStPONpA==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-es/-/protoc-gen-es-1.9.0.tgz",
"integrity": "sha512-LJy1nC3Jsfdhs9v48P7qF6YXIqh+usFhXSVzJDTmw0yKjxQ3CKBNISRtaMql/g9hb1MLRU6unHCcFfdz4HSO/Q==",
"dev": true,
"dependencies": {
"@bufbuild/protobuf": "^1.8.0",
"@bufbuild/protoplugin": "1.8.0"
"@bufbuild/protobuf": "^1.9.0",
"@bufbuild/protoplugin": "1.9.0"
},
"bin": {
"protoc-gen-es": "bin/protoc-gen-es"
@@ -2476,7 +2476,7 @@
"node": ">=14"
},
"peerDependencies": {
"@bufbuild/protobuf": "1.8.0"
"@bufbuild/protobuf": "1.9.0"
},
"peerDependenciesMeta": {
"@bufbuild/protobuf": {
@@ -2485,12 +2485,12 @@
}
},
"node_modules/@bufbuild/protoplugin": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-1.8.0.tgz",
"integrity": "sha512-Pb89cTshW+I577qh27VvxGYvZEvQ3zJ8La1OfzPCKugP9d4A4P65WStkAY+aSCiDHk68m1/+mtBb6elfiLPuFg==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-1.9.0.tgz",
"integrity": "sha512-/mxMiGs5h78RUHT7v4+mv0Wt0gyRf/SOS5PLzKEg2sclEAlFPbXfZ8HjlvxJpXZP/YpP3HvsW/mil3E69G0mXg==",
"dev": true,
"dependencies": {
"@bufbuild/protobuf": "1.8.0",
"@bufbuild/protobuf": "1.9.0",
"@typescript/vfs": "^1.4.0",
"typescript": "4.5.2"
}

View File

@@ -20,7 +20,7 @@
"@angular/platform-browser": "^17.3.0",
"@angular/platform-browser-dynamic": "^17.3.0",
"@angular/router": "^17.3.0",
"@bufbuild/protobuf": "^1.8.0",
"@bufbuild/protobuf": "^1.9.0",
"@connectrpc/connect": "^1.4.0",
"@connectrpc/connect-query": "^1.3.1",
"@connectrpc/connect-web": "^1.4.0",
@@ -32,8 +32,8 @@
"@angular-devkit/build-angular": "^17.3.4",
"@angular/cli": "^17.3.4",
"@angular/compiler-cli": "^17.3.0",
"@bufbuild/buf": "^1.30.1",
"@bufbuild/protoc-gen-es": "^1.8.0",
"@bufbuild/buf": "^1.31.0",
"@bufbuild/protoc-gen-es": "^1.9.0",
"@connectrpc/protoc-gen-connect-es": "^1.4.0",
"@connectrpc/protoc-gen-connect-query": "^1.3.1",
"@types/jasmine": "~5.1.0",
@@ -45,4 +45,4 @@
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.4.2"
}
}
}

View File

@@ -1,9 +1,25 @@
import { ApplicationConfig } from '@angular/core';
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';
// import { provideHttpClient, withFetch } from '@angular/common/http';
import { routes } from './app.routes';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { ConnectModule } from '../connect/connect.module';
import { provideClient } from "../connect/client.provider";
import { UserService } from './gen/holos/v1alpha1/user_connect';
import { OrganizationService } from './gen/holos/v1alpha1/organization_connect';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), provideAnimationsAsync()]
providers: [
provideRouter(routes),
provideAnimationsAsync(),
// provideHttpClient(withFetch()),
provideClient(UserService),
provideClient(OrganizationService),
importProvidersFrom(
ConnectModule.forRoot({
baseUrl: window.location.origin
}),
),
]
};

View File

@@ -0,0 +1,35 @@
// @generated by protoc-gen-connect-query v1.3.1 with parameter "target=ts"
// @generated from file holos/v1alpha1/organization.proto (package holos.v1alpha1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import { MethodKind } from "@bufbuild/protobuf";
import { CreateCallerOrganizationRequest, GetCallerOrganizationsRequest, GetCallerOrganizationsResponse } from "./organization_pb.js";
/**
* @generated from rpc holos.v1alpha1.OrganizationService.GetCallerOrganizations
*/
export const getCallerOrganizations = {
localName: "getCallerOrganizations",
name: "GetCallerOrganizations",
kind: MethodKind.Unary,
I: GetCallerOrganizationsRequest,
O: GetCallerOrganizationsResponse,
service: {
typeName: "holos.v1alpha1.OrganizationService"
}
} as const;
/**
* @generated from rpc holos.v1alpha1.OrganizationService.CreateCallerOrganization
*/
export const createCallerOrganization = {
localName: "createCallerOrganization",
name: "CreateCallerOrganization",
kind: MethodKind.Unary,
I: CreateCallerOrganizationRequest,
O: GetCallerOrganizationsResponse,
service: {
typeName: "holos.v1alpha1.OrganizationService"
}
} as const;

View File

@@ -0,0 +1,35 @@
// @generated by protoc-gen-connect-es v1.4.0 with parameter "target=ts"
// @generated from file holos/v1alpha1/organization.proto (package holos.v1alpha1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import { CreateCallerOrganizationRequest, GetCallerOrganizationsRequest, GetCallerOrganizationsResponse } from "./organization_pb.js";
import { MethodKind } from "@bufbuild/protobuf";
/**
* @generated from service holos.v1alpha1.OrganizationService
*/
export const OrganizationService = {
typeName: "holos.v1alpha1.OrganizationService",
methods: {
/**
* @generated from rpc holos.v1alpha1.OrganizationService.GetCallerOrganizations
*/
getCallerOrganizations: {
name: "GetCallerOrganizations",
I: GetCallerOrganizationsRequest,
O: GetCallerOrganizationsResponse,
kind: MethodKind.Unary,
},
/**
* @generated from rpc holos.v1alpha1.OrganizationService.CreateCallerOrganization
*/
createCallerOrganization: {
name: "CreateCallerOrganization",
I: CreateCallerOrganizationRequest,
O: GetCallerOrganizationsResponse,
kind: MethodKind.Unary,
},
}
} as const;

View File

@@ -0,0 +1,172 @@
// @generated by protoc-gen-es v1.9.0 with parameter "target=ts"
// @generated from file holos/v1alpha1/organization.proto (package holos.v1alpha1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import { Message, proto3 } from "@bufbuild/protobuf";
import { Timestamps } from "./timestamps_pb.js";
import { User } from "./user_pb.js";
/**
* @generated from message holos.v1alpha1.Organization
*/
export class Organization extends Message<Organization> {
/**
* Unique id assigned by the server.
*
* @generated from field: string id = 1;
*/
id = "";
/**
* @generated from field: string name = 2;
*/
name = "";
/**
* @generated from field: string display_name = 3;
*/
displayName = "";
/**
* @generated from field: holos.v1alpha1.Timestamps timestamps = 4;
*/
timestamps?: Timestamps;
constructor(data?: PartialMessage<Organization>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.Organization";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "display_name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 4, name: "timestamps", kind: "message", T: Timestamps },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Organization {
return new Organization().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Organization {
return new Organization().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Organization {
return new Organization().fromJsonString(jsonString, options);
}
static equals(a: Organization | PlainMessage<Organization> | undefined, b: Organization | PlainMessage<Organization> | undefined): boolean {
return proto3.util.equals(Organization, a, b);
}
}
/**
* @generated from message holos.v1alpha1.GetCallerOrganizationsRequest
*/
export class GetCallerOrganizationsRequest extends Message<GetCallerOrganizationsRequest> {
constructor(data?: PartialMessage<GetCallerOrganizationsRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.GetCallerOrganizationsRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetCallerOrganizationsRequest {
return new GetCallerOrganizationsRequest().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetCallerOrganizationsRequest {
return new GetCallerOrganizationsRequest().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetCallerOrganizationsRequest {
return new GetCallerOrganizationsRequest().fromJsonString(jsonString, options);
}
static equals(a: GetCallerOrganizationsRequest | PlainMessage<GetCallerOrganizationsRequest> | undefined, b: GetCallerOrganizationsRequest | PlainMessage<GetCallerOrganizationsRequest> | undefined): boolean {
return proto3.util.equals(GetCallerOrganizationsRequest, a, b);
}
}
/**
* @generated from message holos.v1alpha1.GetCallerOrganizationsResponse
*/
export class GetCallerOrganizationsResponse extends Message<GetCallerOrganizationsResponse> {
/**
* @generated from field: holos.v1alpha1.User user = 1;
*/
user?: User;
/**
* @generated from field: repeated holos.v1alpha1.Organization organizations = 2;
*/
organizations: Organization[] = [];
constructor(data?: PartialMessage<GetCallerOrganizationsResponse>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.GetCallerOrganizationsResponse";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "user", kind: "message", T: User },
{ no: 2, name: "organizations", kind: "message", T: Organization, repeated: true },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetCallerOrganizationsResponse {
return new GetCallerOrganizationsResponse().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetCallerOrganizationsResponse {
return new GetCallerOrganizationsResponse().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetCallerOrganizationsResponse {
return new GetCallerOrganizationsResponse().fromJsonString(jsonString, options);
}
static equals(a: GetCallerOrganizationsResponse | PlainMessage<GetCallerOrganizationsResponse> | undefined, b: GetCallerOrganizationsResponse | PlainMessage<GetCallerOrganizationsResponse> | undefined): boolean {
return proto3.util.equals(GetCallerOrganizationsResponse, a, b);
}
}
/**
* @generated from message holos.v1alpha1.CreateCallerOrganizationRequest
*/
export class CreateCallerOrganizationRequest extends Message<CreateCallerOrganizationRequest> {
constructor(data?: PartialMessage<CreateCallerOrganizationRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.CreateCallerOrganizationRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CreateCallerOrganizationRequest {
return new CreateCallerOrganizationRequest().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CreateCallerOrganizationRequest {
return new CreateCallerOrganizationRequest().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CreateCallerOrganizationRequest {
return new CreateCallerOrganizationRequest().fromJsonString(jsonString, options);
}
static equals(a: CreateCallerOrganizationRequest | PlainMessage<CreateCallerOrganizationRequest> | undefined, b: CreateCallerOrganizationRequest | PlainMessage<CreateCallerOrganizationRequest> | undefined): boolean {
return proto3.util.equals(CreateCallerOrganizationRequest, a, b);
}
}

View File

@@ -0,0 +1,55 @@
// @generated by protoc-gen-es v1.9.0 with parameter "target=ts"
// @generated from file holos/v1alpha1/timestamps.proto (package holos.v1alpha1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import { Message, proto3, Timestamp } from "@bufbuild/protobuf";
/**
* @generated from message holos.v1alpha1.Timestamps
*/
export class Timestamps extends Message<Timestamps> {
/**
* Created at timestamp
*
* @generated from field: google.protobuf.Timestamp created_at = 1;
*/
createdAt?: Timestamp;
/**
* Updated at timestamp
*
* @generated from field: google.protobuf.Timestamp updated_at = 2;
*/
updatedAt?: Timestamp;
constructor(data?: PartialMessage<Timestamps>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.Timestamps";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "created_at", kind: "message", T: Timestamp },
{ no: 2, name: "updated_at", kind: "message", T: Timestamp },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Timestamps {
return new Timestamps().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Timestamps {
return new Timestamps().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Timestamps {
return new Timestamps().fromJsonString(jsonString, options);
}
static equals(a: Timestamps | PlainMessage<Timestamps> | undefined, b: Timestamps | PlainMessage<Timestamps> | undefined): boolean {
return proto3.util.equals(Timestamps, a, b);
}
}

View File

@@ -0,0 +1,49 @@
// @generated by protoc-gen-connect-query v1.3.1 with parameter "target=ts"
// @generated from file holos/v1alpha1/user.proto (package holos.v1alpha1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import { MethodKind } from "@bufbuild/protobuf";
import { CreateCallerUserRequest, CreateCallerUserResponse, GetCallerClaimsRequest, GetCallerClaimsResponse, GetCallerUserRequest, GetCallerUserResponse } from "./user_pb.js";
/**
* @generated from rpc holos.v1alpha1.UserService.GetCallerClaims
*/
export const getCallerClaims = {
localName: "getCallerClaims",
name: "GetCallerClaims",
kind: MethodKind.Unary,
I: GetCallerClaimsRequest,
O: GetCallerClaimsResponse,
service: {
typeName: "holos.v1alpha1.UserService"
}
} as const;
/**
* @generated from rpc holos.v1alpha1.UserService.GetCallerUser
*/
export const getCallerUser = {
localName: "getCallerUser",
name: "GetCallerUser",
kind: MethodKind.Unary,
I: GetCallerUserRequest,
O: GetCallerUserResponse,
service: {
typeName: "holos.v1alpha1.UserService"
}
} as const;
/**
* @generated from rpc holos.v1alpha1.UserService.CreateCallerUser
*/
export const createCallerUser = {
localName: "createCallerUser",
name: "CreateCallerUser",
kind: MethodKind.Unary,
I: CreateCallerUserRequest,
O: CreateCallerUserResponse,
service: {
typeName: "holos.v1alpha1.UserService"
}
} as const;

View File

@@ -0,0 +1,44 @@
// @generated by protoc-gen-connect-es v1.4.0 with parameter "target=ts"
// @generated from file holos/v1alpha1/user.proto (package holos.v1alpha1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import { CreateCallerUserRequest, CreateCallerUserResponse, GetCallerClaimsRequest, GetCallerClaimsResponse, GetCallerUserRequest, GetCallerUserResponse } from "./user_pb.js";
import { MethodKind } from "@bufbuild/protobuf";
/**
* @generated from service holos.v1alpha1.UserService
*/
export const UserService = {
typeName: "holos.v1alpha1.UserService",
methods: {
/**
* @generated from rpc holos.v1alpha1.UserService.GetCallerClaims
*/
getCallerClaims: {
name: "GetCallerClaims",
I: GetCallerClaimsRequest,
O: GetCallerClaimsResponse,
kind: MethodKind.Unary,
},
/**
* @generated from rpc holos.v1alpha1.UserService.GetCallerUser
*/
getCallerUser: {
name: "GetCallerUser",
I: GetCallerUserRequest,
O: GetCallerUserResponse,
kind: MethodKind.Unary,
},
/**
* @generated from rpc holos.v1alpha1.UserService.CreateCallerUser
*/
createCallerUser: {
name: "CreateCallerUser",
I: CreateCallerUserRequest,
O: CreateCallerUserResponse,
kind: MethodKind.Unary,
},
}
} as const;

View File

@@ -0,0 +1,367 @@
// @generated by protoc-gen-es v1.9.0 with parameter "target=ts"
// @generated from file holos/v1alpha1/user.proto (package holos.v1alpha1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import { Message, proto3 } from "@bufbuild/protobuf";
import { Timestamps } from "./timestamps_pb.js";
/**
* User represents a human user in the system. See db schema in ent/schema/user.go
*
* @generated from message holos.v1alpha1.User
*/
export class User extends Message<User> {
/**
* Unique id assigned by the server.
*
* @generated from field: string id = 1;
*/
id = "";
/**
* @generated from field: string email = 2;
*/
email = "";
/**
* @generated from field: bool email_verified = 3;
*/
emailVerified = false;
/**
* @generated from field: string name = 4;
*/
name = "";
/**
* @generated from field: holos.v1alpha1.Timestamps timestamps = 5;
*/
timestamps?: Timestamps;
constructor(data?: PartialMessage<User>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.User";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "email", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "email_verified", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
{ no: 4, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 5, name: "timestamps", kind: "message", T: Timestamps },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): User {
return new User().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): User {
return new User().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): User {
return new User().fromJsonString(jsonString, options);
}
static equals(a: User | PlainMessage<User> | undefined, b: User | PlainMessage<User> | undefined): boolean {
return proto3.util.equals(User, a, b);
}
}
/**
* @generated from message holos.v1alpha1.CreateCallerUserRequest
*/
export class CreateCallerUserRequest extends Message<CreateCallerUserRequest> {
constructor(data?: PartialMessage<CreateCallerUserRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.CreateCallerUserRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CreateCallerUserRequest {
return new CreateCallerUserRequest().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CreateCallerUserRequest {
return new CreateCallerUserRequest().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CreateCallerUserRequest {
return new CreateCallerUserRequest().fromJsonString(jsonString, options);
}
static equals(a: CreateCallerUserRequest | PlainMessage<CreateCallerUserRequest> | undefined, b: CreateCallerUserRequest | PlainMessage<CreateCallerUserRequest> | undefined): boolean {
return proto3.util.equals(CreateCallerUserRequest, a, b);
}
}
/**
* @generated from message holos.v1alpha1.CreateCallerUserResponse
*/
export class CreateCallerUserResponse extends Message<CreateCallerUserResponse> {
/**
* @generated from field: holos.v1alpha1.User user = 1;
*/
user?: User;
constructor(data?: PartialMessage<CreateCallerUserResponse>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.CreateCallerUserResponse";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "user", kind: "message", T: User },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CreateCallerUserResponse {
return new CreateCallerUserResponse().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CreateCallerUserResponse {
return new CreateCallerUserResponse().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CreateCallerUserResponse {
return new CreateCallerUserResponse().fromJsonString(jsonString, options);
}
static equals(a: CreateCallerUserResponse | PlainMessage<CreateCallerUserResponse> | undefined, b: CreateCallerUserResponse | PlainMessage<CreateCallerUserResponse> | undefined): boolean {
return proto3.util.equals(CreateCallerUserResponse, a, b);
}
}
/**
* @generated from message holos.v1alpha1.GetCallerClaimsRequest
*/
export class GetCallerClaimsRequest extends Message<GetCallerClaimsRequest> {
constructor(data?: PartialMessage<GetCallerClaimsRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.GetCallerClaimsRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetCallerClaimsRequest {
return new GetCallerClaimsRequest().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetCallerClaimsRequest {
return new GetCallerClaimsRequest().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetCallerClaimsRequest {
return new GetCallerClaimsRequest().fromJsonString(jsonString, options);
}
static equals(a: GetCallerClaimsRequest | PlainMessage<GetCallerClaimsRequest> | undefined, b: GetCallerClaimsRequest | PlainMessage<GetCallerClaimsRequest> | undefined): boolean {
return proto3.util.equals(GetCallerClaimsRequest, a, b);
}
}
/**
* @generated from message holos.v1alpha1.Claims
*/
export class Claims extends Message<Claims> {
/**
* @generated from field: string iss = 1;
*/
iss = "";
/**
* @generated from field: string sub = 2;
*/
sub = "";
/**
* @generated from field: string email = 3;
*/
email = "";
/**
* @generated from field: bool email_verified = 4;
*/
emailVerified = false;
/**
* @generated from field: string name = 5;
*/
name = "";
/**
* @generated from field: repeated string groups = 6;
*/
groups: string[] = [];
/**
* @generated from field: string given_name = 7;
*/
givenName = "";
/**
* @generated from field: string family_name = 8;
*/
familyName = "";
/**
* @generated from field: string picture = 9;
*/
picture = "";
constructor(data?: PartialMessage<Claims>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.Claims";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "iss", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "sub", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "email", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 4, name: "email_verified", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
{ no: 5, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 6, name: "groups", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true },
{ no: 7, name: "given_name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 8, name: "family_name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 9, name: "picture", kind: "scalar", T: 9 /* ScalarType.STRING */ },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Claims {
return new Claims().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Claims {
return new Claims().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Claims {
return new Claims().fromJsonString(jsonString, options);
}
static equals(a: Claims | PlainMessage<Claims> | undefined, b: Claims | PlainMessage<Claims> | undefined): boolean {
return proto3.util.equals(Claims, a, b);
}
}
/**
* UserClaims represents id token claims
*
* @generated from message holos.v1alpha1.GetCallerClaimsResponse
*/
export class GetCallerClaimsResponse extends Message<GetCallerClaimsResponse> {
/**
* @generated from field: holos.v1alpha1.Claims claims = 1;
*/
claims?: Claims;
constructor(data?: PartialMessage<GetCallerClaimsResponse>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.GetCallerClaimsResponse";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "claims", kind: "message", T: Claims },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetCallerClaimsResponse {
return new GetCallerClaimsResponse().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetCallerClaimsResponse {
return new GetCallerClaimsResponse().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetCallerClaimsResponse {
return new GetCallerClaimsResponse().fromJsonString(jsonString, options);
}
static equals(a: GetCallerClaimsResponse | PlainMessage<GetCallerClaimsResponse> | undefined, b: GetCallerClaimsResponse | PlainMessage<GetCallerClaimsResponse> | undefined): boolean {
return proto3.util.equals(GetCallerClaimsResponse, a, b);
}
}
/**
* Empty request, claims are pulled from the id token
*
* @generated from message holos.v1alpha1.GetCallerUserRequest
*/
export class GetCallerUserRequest extends Message<GetCallerUserRequest> {
constructor(data?: PartialMessage<GetCallerUserRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.GetCallerUserRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetCallerUserRequest {
return new GetCallerUserRequest().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetCallerUserRequest {
return new GetCallerUserRequest().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetCallerUserRequest {
return new GetCallerUserRequest().fromJsonString(jsonString, options);
}
static equals(a: GetCallerUserRequest | PlainMessage<GetCallerUserRequest> | undefined, b: GetCallerUserRequest | PlainMessage<GetCallerUserRequest> | undefined): boolean {
return proto3.util.equals(GetCallerUserRequest, a, b);
}
}
/**
* @generated from message holos.v1alpha1.GetCallerUserResponse
*/
export class GetCallerUserResponse extends Message<GetCallerUserResponse> {
/**
* @generated from field: holos.v1alpha1.User user = 1;
*/
user?: User;
constructor(data?: PartialMessage<GetCallerUserResponse>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.GetCallerUserResponse";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "user", kind: "message", T: User },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetCallerUserResponse {
return new GetCallerUserResponse().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetCallerUserResponse {
return new GetCallerUserResponse().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetCallerUserResponse {
return new GetCallerUserResponse().fromJsonString(jsonString, options);
}
static equals(a: GetCallerUserResponse | PlainMessage<GetCallerUserResponse> | undefined, b: GetCallerUserResponse | PlainMessage<GetCallerUserResponse> | undefined): boolean {
return proto3.util.equals(GetCallerUserResponse, a, b);
}
}

View File

@@ -3,7 +3,9 @@
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
[mode]="(isHandset$ | async) ? 'over' : 'side'"
[opened]="(isHandset$ | async) === false">
<mat-toolbar>Menu</mat-toolbar>
<mat-toolbar>
<span>Menu</span>
</mat-toolbar>
<mat-nav-list>
<a mat-list-item routerLink="/home" routerLinkActive="active-link">Home</a>
<a mat-list-item routerLink="/clusters" routerLinkActive="active-link">Clusters</a>
@@ -21,6 +23,15 @@
</button>
}
<span>Holos</span>
<span class="toolbar-spacer"></span>
<span>
@if (org$ | async; as org) {
<button mat-button (click)="refreshOrg()">
{{ org.displayName }}
</button>
}
</span>
<app-profile-button [claims$]="claims$"></app-profile-button>
</mat-toolbar>
<!-- Add Content Here -->
<router-outlet></router-outlet>

View File

@@ -15,3 +15,22 @@
top: 0;
z-index: 1;
}
.toolbar-spacer {
flex: 1 1 auto;
}
.avatar-container {
height: 100%;
width: 100%;
border-radius: 50%;
}
button {
&.image {
background-color: transparent;
background-repeat: no-repeat;
background-size: cover;
background-position: center center;
}
}

View File

@@ -1,6 +1,6 @@
import { Component, inject } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { AsyncPipe } from '@angular/common';
import { AsyncPipe, NgIf } from '@angular/common';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { MatSidenavModule } from '@angular/material/sidenav';
@@ -9,6 +9,12 @@ import { MatIconModule } from '@angular/material/icon';
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
import { Claims } from '../gen/holos/v1alpha1/user_pb';
import { ProfileButtonComponent } from '../profile-button/profile-button.component';
import { UserService } from '../services/user.service';
import { Organization } from '../gen/holos/v1alpha1/organization_pb';
import { OrganizationService } from '../services/organization.service';
@Component({
selector: 'app-nav',
@@ -21,18 +27,35 @@ import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
MatSidenavModule,
MatListModule,
MatIconModule,
NgIf,
AsyncPipe,
RouterLink,
RouterLinkActive,
RouterOutlet,
MatCardModule,
ProfileButtonComponent,
]
})
export class NavComponent {
export class NavComponent implements OnInit {
private breakpointObserver = inject(BreakpointObserver);
private userService = inject(UserService);
private orgService = inject(OrganizationService);
claims$!: Observable<Claims | null>;
org$!: Observable<Organization | undefined>;
refreshOrg(): void {
this.orgService.refreshOrganizations()
}
isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset)
.pipe(
map(result => result.matches),
shareReplay()
);
ngOnInit(): void {
this.claims$ = this.userService.getClaims();
this.org$ = this.orgService.activeOrg();
}
}

View File

@@ -0,0 +1,28 @@
@if (claims$ | async; as claims) {
<button mat-icon-button [matMenuTriggerFor]="menu">
@if (claims.picture) {
<img class="profile-picture" [src]="claims.picture" alt="Profile"/>
} @else {
<mat-icon>account_circle</mat-icon>
}
</button>
<mat-menu class="accounts-menu" #menu="matMenu">
<mat-card class="accounts-card">
<mat-card-header>
<div mat-card-avatar class="accounts-header-image" [ngStyle]="{'background-image': claims.picture ? 'url(' + claims.picture +')' : 'url(/ui/assets/img/account_circle.svg)'}">
</div>
<mat-card-title>{{ claims.name }}</mat-card-title>
<mat-card-subtitle>{{ claims.email }}</mat-card-subtitle>
</mat-card-header>
<mat-card-actions>
<a mat-menu-item href="{{claims.iss}}/ui/console/users/me?id=general">
Profile
</a>
<a mat-menu-item href="{{claims.iss}}/oidc/v1/end_session">
Logout
</a>
</mat-card-actions>
</mat-card>
</mat-menu>
}

View File

@@ -0,0 +1,16 @@
.profile-picture {
border-radius: 50%;
}
.accounts-card {
max-width: 400px;
}
.accounts-header-image {
background-image: url('/ui/assets/img/account_circle.svg');
background-size: cover;
}
::ng-deep .mat-mdc-menu-content {
padding: 0px !important;
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProfileButtonComponent } from './profile-button.component';
describe('ProfileButtonComponent', () => {
let component: ProfileButtonComponent;
let fixture: ComponentFixture<ProfileButtonComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProfileButtonComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ProfileButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,27 @@
import { Component, Input } from '@angular/core';
import { Claims } from '../gen/holos/v1alpha1/user_pb';
import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { Observable } from 'rxjs';
import { AsyncPipe, NgIf, NgStyle } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-profile-button',
standalone: true,
imports: [
MatButtonModule,
MatMenuModule,
MatIconModule,
MatCardModule,
AsyncPipe,
NgIf,
NgStyle,
],
templateUrl: './profile-button.component.html',
styleUrl: './profile-button.component.scss'
})
export class ProfileButtonComponent {
@Input({ required: true }) claims$!: Observable<Claims | null>;
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { OrganizationService } from './organization.service';
describe('OrganizationService', () => {
let service: OrganizationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(OrganizationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,67 @@
import { Inject, Injectable } from '@angular/core';
import { OrganizationService as ConnectOrganizationService } from '../gen/holos/v1alpha1/organization_connect';
import { ObservableClient } from '../../connect/observable-client';
import { Observable, switchMap, of, shareReplay, catchError, BehaviorSubject } from 'rxjs';
import { GetCallerOrganizationsResponse, Organization } from '../gen/holos/v1alpha1/organization_pb';
import { UserService } from './user.service';
import { Code, ConnectError } from '@connectrpc/connect';
@Injectable({
providedIn: 'root'
})
export class OrganizationService {
private callerOrganizationsTrigger$ = new BehaviorSubject<void>(undefined);
private callerOrganizations$: Observable<GetCallerOrganizationsResponse>;
private fetchCallerOrganizations(): Observable<GetCallerOrganizationsResponse> {
return this.client.getCallerOrganizations({ request: {} }).pipe(
switchMap(resp => {
if (resp && resp.organizations.length > 0) {
return of(resp)
}
return this.client.createCallerOrganization({ request: {} })
}),
catchError(err => {
if (err instanceof ConnectError) {
if (err.code == Code.NotFound) {
return this.userService.createUser().pipe(
switchMap(user => this.client.createCallerOrganization({ request: {} }))
)
}
}
console.error('Error fetching data:', err);
throw err;
}),
)
}
getOrganizations(): Observable<Organization[]> {
return this.callerOrganizations$.pipe(
switchMap(resp => of(resp.organizations))
)
}
activeOrg(): Observable<Organization | undefined> {
return this.callerOrganizations$.pipe(
switchMap(resp => of(resp.organizations.at(-1)))
)
}
refreshOrganizations(): void {
this.callerOrganizationsTrigger$.next()
}
constructor(
@Inject(ConnectOrganizationService) private client: ObservableClient<typeof ConnectOrganizationService>,
private userService: UserService,
) {
this.callerOrganizations$ = this.callerOrganizationsTrigger$.pipe(
switchMap(() => this.fetchCallerOrganizations()),
shareReplay(1),
catchError(err => {
console.error('Error fetching data:', err);
throw err;
})
);
}
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(UserService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,42 @@
import { Inject, Injectable } from '@angular/core';
import { Observable, switchMap, of, shareReplay } from 'rxjs';
import { ObservableClient } from '../../connect/observable-client';
import { Claims, User } from '../gen/holos/v1alpha1/user_pb';
import { UserService as ConnectUserService } from '../gen/holos/v1alpha1/user_connect';
@Injectable({
providedIn: 'root'
})
export class UserService {
getClaims(): Observable<Claims | null> {
return this.client.getCallerClaims({ request: {} }).pipe(
switchMap(getCallerClaimsResponse => {
if (getCallerClaimsResponse && getCallerClaimsResponse.claims) {
return of(getCallerClaimsResponse.claims)
} else {
return of(null)
}
}),
// Consolidate to one api call for all subscribers
shareReplay(1)
)
}
createUser(): Observable<User | null> {
return this.client.createCallerUser({ request: {} }).pipe(
switchMap(resp => {
if (resp && resp.user) {
return of(resp.user)
} else {
return of(null)
}
}),
shareReplay(1)
)
}
constructor(
@Inject(ConnectUserService) private client: ObservableClient<typeof ConnectUserService>,
) { }
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@@ -0,0 +1,15 @@
import { Provider } from "@angular/core";
import { Transport } from "@connectrpc/connect";
import { ServiceType } from "@bufbuild/protobuf";
import { createObservableClient } from "./observable-client";
import { TRANSPORT } from "./transport.token";
export function provideClient<T extends ServiceType>(service: T): Provider {
return {
provide: service,
useFactory: (transport: Transport) => {
return createObservableClient(service, transport);
},
deps: [TRANSPORT],
};
}

View File

@@ -0,0 +1,30 @@
import { ModuleWithProviders, NgModule } from '@angular/core'
import { Interceptor } from '@connectrpc/connect'
import { createConnectTransport } from '@connectrpc/connect-web'
import { INTERCEPTORS } from './interceptor.token'
import { TRANSPORT } from './transport.token'
@NgModule()
export class ConnectModule {
public static forRoot(
connectOptions: Omit<
Parameters<typeof createConnectTransport>[0],
'interceptors'
>
): ModuleWithProviders<ConnectModule> {
return {
ngModule: ConnectModule,
providers: [
{
provide: TRANSPORT,
useFactory: (interceptors: Interceptor[]) =>
createConnectTransport({
...connectOptions,
interceptors: interceptors,
}),
deps: [INTERCEPTORS],
},
],
}
}
}

View File

@@ -0,0 +1,30 @@
import { ModuleWithProviders, NgModule } from '@angular/core'
import { Interceptor } from '@connectrpc/connect'
import { createGrpcWebTransport } from '@connectrpc/connect-web'
import { INTERCEPTORS } from './interceptor.token'
import { TRANSPORT } from './transport.token'
@NgModule()
export class GrpcWebModule {
public static forRoot(
grpcWebOptions: Omit<
Parameters<typeof createGrpcWebTransport>[0],
'interceptors'
>
): ModuleWithProviders<GrpcWebModule> {
return {
ngModule: GrpcWebModule,
providers: [
{
provide: TRANSPORT,
useFactory: (interceptors: Interceptor[]) =>
createGrpcWebTransport({
...grpcWebOptions,
interceptors: interceptors,
}),
deps: [INTERCEPTORS],
},
],
}
}
}

View File

@@ -0,0 +1,9 @@
import { InjectionToken } from '@angular/core'
import type { Interceptor } from '@connectrpc/connect'
export const INTERCEPTORS = new InjectionToken<Interceptor[]>(
'connect.interceptors',
{
factory: () => [],
}
)

View File

@@ -0,0 +1,120 @@
import { makeAnyClient, CallOptions, Transport } from '@connectrpc/connect'
import { createAsyncIterable } from '@connectrpc/connect/protocol'
import {
ServiceType,
PartialMessage,
MethodInfoServerStreaming,
MethodInfo,
MethodInfoUnary,
MethodKind,
Message,
} from '@bufbuild/protobuf'
import { Observable } from 'rxjs'
export type ObservableClient<T extends ServiceType> = {
[P in keyof T['methods']]: T['methods'][P] extends MethodInfoUnary<
infer I,
infer O
>
? UnaryFn<I, O>
: T['methods'][P] extends MethodInfoServerStreaming<infer I, infer O>
? ServerStreamingFn<I, O>
: never
}
export function createObservableClient<T extends ServiceType>(
service: T,
transport: Transport
) {
return makeAnyClient(service, (method) => {
switch (method.kind) {
case MethodKind.Unary:
return createUnaryFn(transport, service, method)
case MethodKind.ServerStreaming:
return createServerStreamingFn(transport, service, method)
default:
return null
}
}) as ObservableClient<T>
}
type UnaryFn<I extends Message<I>, O extends Message<O>> = (
request: PartialMessage<I>,
options?: CallOptions
) => Observable<O>
function createUnaryFn<I extends Message<I>, O extends Message<O>>(
transport: Transport,
service: ServiceType,
method: MethodInfo<I, O>
): UnaryFn<I, O> {
return function (requestMessage, options) {
return new Observable<O>((subscriber) => {
transport
.unary(
service,
method,
options?.signal,
options?.timeoutMs,
options?.headers,
requestMessage
)
.then(
(response) => {
options?.onHeader?.(response.header)
subscriber.next(response.message)
options?.onTrailer?.(response.trailer)
},
(err) => {
subscriber.error(err)
}
)
.finally(() => {
subscriber.complete()
})
})
}
}
type ServerStreamingFn<I extends Message<I>, O extends Message<O>> = (
request: PartialMessage<I>,
options?: CallOptions
) => Observable<O>
export function createServerStreamingFn<
I extends Message<I>,
O extends Message<O>
>(
transport: Transport,
service: ServiceType,
method: MethodInfo<I, O>
): ServerStreamingFn<I, O> {
return function (input, options) {
return new Observable<O>((subscriber) => {
transport
.stream<I, O>(
service,
method,
options?.signal,
options?.timeoutMs,
options?.headers,
createAsyncIterable([input])
)
.then(
async (streamResponse) => {
options?.onHeader?.(streamResponse.header)
for await (const response of streamResponse.message) {
subscriber.next(response)
}
options?.onTrailer?.(streamResponse.trailer)
},
(err) => {
subscriber.error(err)
}
)
.finally(() => {
subscriber.complete()
})
})
}
}

View File

@@ -0,0 +1,4 @@
import { InjectionToken } from "@angular/core";
import type { Transport } from "@connectrpc/connect";
export const TRANSPORT = new InjectionToken<Transport>("connect.transport");

View File

@@ -3,7 +3,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
"types": [],
},
"files": [
"src/main.ts"

View File

@@ -11,6 +11,7 @@ import (
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/logger"
"github.com/holos-run/holos/internal/server/middleware/authn"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
@@ -282,6 +283,7 @@ func getenv(key, defaultValue string) string {
type ServerConfig struct {
oidcIssuer string // --oidc-issuer
oidcAudiences stringSlice // --oidc-audience
authHeader string // --auth-header
listenAndServe bool // --no-serve
listenPort int // --listen-port
metricsPort int // --metrics-port
@@ -299,6 +301,10 @@ func (c *ServerConfig) OIDCAudiences() []string {
return c.oidcAudiences
}
func (c *ServerConfig) AuthHeader() string {
return c.authHeader
}
// DatabaseURI represents the database connection uri.
func (c *ServerConfig) DatabaseURI() string {
return c.databaseURI
@@ -326,6 +332,7 @@ func (c *ServerConfig) FlagSet() *flag.FlagSet {
f := flag.NewFlagSet("", flag.ContinueOnError)
f.StringVar(&c.oidcIssuer, "oidc-issuer", c.oidcIssuer, "oidc issuer url.")
f.Var(&c.oidcAudiences, "oidc-audience", "allowed oidc audiences.")
f.StringVar(&c.authHeader, "auth-header", authn.Header, "bearer token header.")
f.BoolVar(&c.listenAndServe, "serve", true, "listen and serve requests.")
f.IntVar(&c.listenPort, "listen-port", 3000, "service listen port.")
f.IntVar(&c.metricsPort, "metrics-port", 9090, "metrics listen port.")

View File

@@ -1,53 +1 @@
package handler
import (
"context"
"connectrpc.com/connect"
"github.com/holos-run/holos/internal/ent"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/server/middleware/authn"
holos "github.com/holos-run/holos/service/gen/holos/v1alpha1"
"google.golang.org/protobuf/types/known/timestamppb"
)
func NewHolosHandler(db *ent.Client) *HolosHandler {
return &HolosHandler{db: db}
}
// HolosHandler implements the connect service handler interface.
type HolosHandler struct {
db *ent.Client
}
func (h *HolosHandler) GetUserClaims(
ctx context.Context,
req *connect.Request[holos.GetUserClaimsRequest],
) (*connect.Response[holos.GetUserClaimsResponse], error) {
authnIdentity, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
res := connect.NewResponse(&holos.GetUserClaimsResponse{
Iss: authnIdentity.Issuer(),
Sub: authnIdentity.Subject(),
Email: authnIdentity.Email(),
EmailVerified: authnIdentity.Verified(),
Name: authnIdentity.Name(),
})
return res, nil
}
// UserToRPC returns an *holos.User adapted from *ent.User u.
func UserToRPC(u *ent.User) *holos.User {
iamUser := holos.User{
Id: u.ID.String(),
Email: u.Email,
Name: u.Name,
Timestamps: &holos.Timestamps{
CreatedAt: timestamppb.New(u.CreatedAt),
UpdatedAt: timestamppb.New(u.UpdatedAt),
},
}
return &iamUser
}

View File

@@ -0,0 +1,143 @@
package handler
import (
"context"
"fmt"
"math/rand"
"strings"
"unicode"
"connectrpc.com/connect"
"github.com/holos-run/holos/internal/ent"
"github.com/holos-run/holos/internal/ent/user"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/server/middleware/authn"
holos "github.com/holos-run/holos/service/gen/holos/v1alpha1"
"google.golang.org/protobuf/types/known/timestamppb"
)
// NewOrganizationHandler returns a new OrganizationService implementation.
func NewOrganizationHandler(db *ent.Client) *OrganizationHandler {
return &OrganizationHandler{db: db}
}
// OrganizationHandler implements the OrganizationService interface.
type OrganizationHandler struct {
db *ent.Client
}
func (h *OrganizationHandler) GetCallerOrganizations(
ctx context.Context,
req *connect.Request[holos.GetCallerOrganizationsRequest],
) (*connect.Response[holos.GetCallerOrganizationsResponse], error) {
authnID, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
dbUser, err := h.db.User.Query().
Where(
user.Iss(authnID.Issuer()),
user.Sub(authnID.Subject()),
).
WithOrganizations().
Only(ctx)
if err != nil {
if ent.MaskNotFound(err) == nil {
return nil, connect.NewError(connect.CodeNotFound, errors.Wrap(err))
} else {
return nil, connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err))
}
}
rpcOrgs := make([]*holos.Organization, 0, len(dbUser.Edges.Organizations))
for _, dbOrg := range dbUser.Edges.Organizations {
rpcOrgs = append(rpcOrgs, OrganizationToRPC(dbOrg))
}
res := connect.NewResponse(&holos.GetCallerOrganizationsResponse{
User: UserToRPC(dbUser),
Organizations: rpcOrgs,
})
return res, nil
}
func (h *OrganizationHandler) CreateCallerOrganization(
ctx context.Context,
req *connect.Request[holos.CreateCallerOrganizationRequest],
) (*connect.Response[holos.GetCallerOrganizationsResponse], error) {
authnID, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
// todo get user by iss, sub
dbUser, err := getUser(ctx, h.db, authnID.Email())
if err != nil {
if ent.MaskNotFound(err) == nil {
return nil, connect.NewError(connect.CodeNotFound, errors.Wrap(err))
} else {
return nil, connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err))
}
}
var org *ent.Organization
err = WithTx(ctx, h.db, func(tx *ent.Tx) (err error) {
org, err = h.db.Organization.Create().
SetName(cleanAndAppendRandom(authnID.Name())).
SetDisplayName(authnID.GivenName() + "'s Org").
SetCreatorID(dbUser.ID).
Save(ctx)
if err != nil {
return err
}
dbUser, err = dbUser.Update().
AddOrganizations(org).
Save(ctx)
return err
})
if err != nil {
return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err))
}
// TODO: prefetch organizations
dbOrgs, err := dbUser.QueryOrganizations().All(ctx)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err))
}
rpcOrgs := make([]*holos.Organization, 0, len(dbOrgs))
for _, dbOrg := range dbOrgs {
rpcOrgs = append(rpcOrgs, OrganizationToRPC(dbOrg))
}
res := connect.NewResponse(&holos.GetCallerOrganizationsResponse{
User: UserToRPC(dbUser),
Organizations: rpcOrgs,
})
return res, nil
}
func cleanAndAppendRandom(s string) string {
mapping := func(r rune) rune {
if unicode.IsLetter(r) {
return unicode.ToLower(r)
}
return -1
}
cleaned := strings.Map(mapping, s)
randNum := rand.Intn(900_000) + 100_000
return fmt.Sprintf("%s-%06d", cleaned, randNum)
}
// OrganizationToRPC returns an *holos.Organization adapted from *ent.Organization u.
func OrganizationToRPC(org *ent.Organization) *holos.Organization {
rpcEntity := holos.Organization{
Id: org.ID.String(),
Name: org.Name,
DisplayName: org.DisplayName,
Timestamps: &holos.Timestamps{
CreatedAt: timestamppb.New(org.CreatedAt),
UpdatedAt: timestamppb.New(org.UpdatedAt),
},
}
return &rpcEntity
}

View File

@@ -6,12 +6,117 @@ import (
"connectrpc.com/connect"
"github.com/holos-run/holos/internal/ent"
"github.com/holos-run/holos/internal/ent/user"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/server/middleware/authn"
"github.com/holos-run/holos/internal/server/middleware/logger"
holos "github.com/holos-run/holos/service/gen/holos/v1alpha1"
"google.golang.org/protobuf/types/known/timestamppb"
)
// NewUserHandler returns a new UserService implementation.
func NewUserHandler(db *ent.Client) *UserHandler {
return &UserHandler{db: db}
}
// UserHandler implements the UserService interface.
type UserHandler struct {
db *ent.Client
}
func (h *UserHandler) GetCallerClaims(
ctx context.Context,
req *connect.Request[holos.GetCallerClaimsRequest],
) (*connect.Response[holos.GetCallerClaimsResponse], error) {
authnID, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
res := connect.NewResponse(&holos.GetCallerClaimsResponse{
Claims: &holos.Claims{
Iss: authnID.Issuer(),
Sub: authnID.Subject(),
Email: authnID.Email(),
EmailVerified: authnID.Verified(),
Name: authnID.Name(),
Groups: authnID.Groups(),
GivenName: authnID.GivenName(),
FamilyName: authnID.FamilyName(),
Picture: authnID.Picture(),
},
})
return res, nil
}
func (h *UserHandler) GetCallerUser(
ctx context.Context,
req *connect.Request[holos.GetCallerUserRequest],
) (*connect.Response[holos.GetCallerUserResponse], error) {
authnID, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
dbUser, err := getUser(ctx, h.db, authnID.Email())
if err != nil {
if ent.MaskNotFound(err) == nil {
return nil, connect.NewError(connect.CodeNotFound, errors.Wrap(err))
} else {
return nil, connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err))
}
}
res := connect.NewResponse(&holos.GetCallerUserResponse{User: UserToRPC(dbUser)})
return res, nil
}
func (h *UserHandler) CreateCallerUser(
ctx context.Context,
req *connect.Request[holos.CreateCallerUserRequest],
) (*connect.Response[holos.CreateCallerUserResponse], error) {
authnID, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
var createdUser *ent.User
err = WithTx(ctx, h.db, func(tx *ent.Tx) error {
createdUser, err = createUser(ctx, tx.Client(), authnID.Name(), authnID)
return err
})
if err != nil {
slog.ErrorContext(ctx, "could not save transaction", "err", err)
return nil, err
}
res := connect.NewResponse(&holos.CreateCallerUserResponse{
User: UserToRPC(createdUser),
})
return res, nil
}
// UserToRPC returns an *holos.User adapted from *ent.User u.
func UserToRPC(u *ent.User) *holos.User {
iamUser := holos.User{
Id: u.ID.String(),
Email: u.Email,
Name: u.Name,
Timestamps: &holos.Timestamps{
CreatedAt: timestamppb.New(u.CreatedAt),
UpdatedAt: timestamppb.New(u.UpdatedAt),
},
}
return &iamUser
}
func getUser(ctx context.Context, client *ent.Client, email string) (*ent.User, error) {
log := logger.FromContext(ctx)
user, err := client.User.Query().Where(user.Email(email)).Only(ctx)
if err != nil {
log.DebugContext(ctx, "could not get user", "err", err, "email", email)
return nil, errors.Wrap(err)
}
return user, nil
}
func createUser(ctx context.Context, client *ent.Client, name string, claims authn.Identity) (*ent.User, error) {
log := logger.FromContext(ctx)
// Create the user, error if it already exists
@@ -20,7 +125,7 @@ func createUser(ctx context.Context, client *ent.Client, name string, claims aut
SetEmail(claims.Email()).
SetIss(claims.Issuer()).
SetSub(claims.Subject()).
SetName(claims.Name()).
SetName(name).
Save(ctx)
if err != nil {
err = connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err))
@@ -29,43 +134,7 @@ func createUser(ctx context.Context, client *ent.Client, name string, claims aut
}
log = log.With("user", user)
log.DebugContext(ctx, "created user")
log.InfoContext(ctx, "created user")
return user, nil
}
func (h *HolosHandler) RegisterUser(
ctx context.Context,
req *connect.Request[holos.RegisterUserRequest],
) (*connect.Response[holos.RegisterUserResponse], error) {
log := logger.FromContext(ctx).With("issue", 127)
oidc, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
var name string
if req.Msg.Name != nil {
name = req.Msg.GetName()
} else {
name = oidc.Name()
}
var createdUser *ent.User
err = WithTx(ctx, h.db, func(tx *ent.Tx) error {
createdUser, err = createUser(ctx, tx.Client(), name, oidc)
return err
})
if err != nil {
slog.ErrorContext(ctx, "could not save transaction", "err", err)
return nil, err
}
log = log.With("user.id", createdUser.ID, "user.name", createdUser.Name)
log.InfoContext(ctx, "registered user", "event", "registration")
res := connect.NewResponse(&holos.RegisterUserResponse{
User: UserToRPC(createdUser),
})
return res, nil
}

View File

@@ -15,6 +15,8 @@ import (
"github.com/holos-run/holos/internal/server/middleware/logger"
)
const Header = "x-oidc-id-token"
// Verifier is the interface that wraps the basic Verify method to verify an
// oidc id token is authentic. Intended for use in request handlers.
type Verifier interface {
@@ -45,6 +47,14 @@ type Identity interface {
Verified() bool
// Name is usually set on the initial id token, often omitted by google in refreshed id tokens.
Name() string
// Groups is the groups claim.
Groups() []string
// GivenName is the given name of the user.
GivenName() string
// FamilyName is the family name of the user.
FamilyName() string
// Picture is an optional avatar image url for the user.
Picture() string
}
// key is an unexported type for keys defined in this package to prevent
@@ -100,11 +110,15 @@ func NewVerifier(ctx context.Context, log *slog.Logger, issuer string) (*oidc.ID
}
type claims struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Email string `json:"email"`
Verified bool `json:"email_verified"`
Name string `json:"name"`
Issuer string `json:"iss"`
Subject string `json:"sub"`
Email string `json:"email"`
Verified bool `json:"email_verified"`
Name string `json:"name"`
Groups []string `json:"groups"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Picture string `json:"picture"`
}
type user struct {
@@ -127,13 +141,29 @@ func (u user) Email() string {
return u.claims.Email
}
func (u user) Groups() []string {
return u.claims.Groups
}
func (u user) GivenName() string {
return u.claims.GivenName
}
func (u user) FamilyName() string {
return u.claims.FamilyName
}
func (u user) Picture() string {
return u.claims.Picture
}
func (u user) Verified() bool {
return u.claims.Verified
}
// Handler returns a handler that verifies the request is authentic and adds a
// Identity to the request context.
func Handler(v Verifier, allowedAudiences []string, next http.Handler) http.Handler {
func Handler(v Verifier, allowedAudiences []string, header string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var rawIDToken string
start := time.Now()
@@ -142,7 +172,7 @@ func Handler(v Verifier, allowedAudiences []string, next http.Handler) http.Hand
// Check the X-Auth-Request-Access-Token header set by Istio ExternalAuthorization
if rawIDToken == "" {
rawIDToken = r.Header.Get("X-Auth-Request-Access-Token")
rawIDToken = r.Header.Get(header)
}
// Validate the authorization bearer token

View File

@@ -145,7 +145,7 @@ func (hf *handlerFactory) NewHandler(t testing.TB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := logger.NewContext(r.Context(), testutils.TestLogger(t))
r = r.WithContext(ctx)
authn.Handler(hf.verifier, []string{clientID}, http.HandlerFunc(myHandler)).ServeHTTP(w, r)
authn.Handler(hf.verifier, []string{clientID}, authn.Header, http.HandlerFunc(myHandler)).ServeHTTP(w, r)
})
}

View File

@@ -8,6 +8,7 @@ import (
"sync/atomic"
"connectrpc.com/connect"
"connectrpc.com/grpcreflect"
"connectrpc.com/validate"
"github.com/holos-run/holos/internal/ent"
"github.com/holos-run/holos/internal/errors"
@@ -91,6 +92,17 @@ func (s *Server) registerHandlers() {
s.mux.Handle("/", s.middlewares(s.notFoundHandler()))
}
// handle wraps handler with holos authentication and registers the handler with the server mux.
func (s *Server) handle(pattern string, handler http.Handler) {
authenticatingHandler := authn.Handler(
s.authenticator,
s.cfg.ServerConfig.OIDCAudiences(),
s.cfg.ServerConfig.AuthHeader(),
handler,
)
s.mux.Handle(pattern, authenticatingHandler)
}
func (s *Server) registerConnectRpc() error {
// Validator for all rpc messages
validator, err := validate.NewInterceptor()
@@ -98,10 +110,18 @@ func (s *Server) registerConnectRpc() error {
return errors.Wrap(fmt.Errorf("could not initialize proto validation interceptor: %w", err))
}
h := handler.NewHolosHandler(s.db)
holosPath, holosHandler := holosconnect.NewHolosServiceHandler(h, connect.WithInterceptors(validator))
authenticatingHandler := authn.Handler(s.authenticator, s.cfg.ServerConfig.OIDCAudiences(), holosHandler)
s.mux.Handle(holosPath, s.middlewares(authenticatingHandler))
opts := connect.WithInterceptors(validator)
s.handle(holosconnect.NewUserServiceHandler(handler.NewUserHandler(s.db), opts))
s.handle(holosconnect.NewOrganizationServiceHandler(handler.NewOrganizationHandler(s.db), opts))
reflector := grpcreflect.NewStaticReflector(
holosconnect.UserServiceName,
holosconnect.OrganizationServiceName,
)
s.mux.Handle(grpcreflect.NewHandlerV1(reflector))
s.mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector))
return nil
}

View File

@@ -0,0 +1,34 @@
syntax = "proto3";
package holos.v1alpha1;
option go_package = "github.com/holos-run/holos/service/gen/holos/v1alpha1;holos";
// git clone https://github.com/bufbuild/protovalidate then add <parent>/protovalidate/proto/protovalidate to your editor proto search path
import "buf/validate/validate.proto";
import "holos/v1alpha1/timestamps.proto";
import "holos/v1alpha1/user.proto";
// For validation, see the [Standard constraints](https://github.com/bufbuild/protovalidate/blob/main/docs/standard-constraints.md)
message Organization {
// Unique id assigned by the server.
string id = 1 [(buf.validate.field).string.uuid = true];
string name = 2 [(buf.validate.field).string.max_len = 100];
string display_name = 3 [(buf.validate.field).string.max_len = 100];
Timestamps timestamps = 4;
}
message GetCallerOrganizationsRequest {}
message GetCallerOrganizationsResponse {
User user = 1;
repeated Organization organizations = 2;
}
message CreateCallerOrganizationRequest {}
service OrganizationService {
rpc GetCallerOrganizations(GetCallerOrganizationsRequest) returns (GetCallerOrganizationsResponse) {}
rpc CreateCallerOrganization(CreateCallerOrganizationRequest) returns (GetCallerOrganizationsResponse) {}
}

View File

@@ -0,0 +1,18 @@
syntax = "proto3";
package holos.v1alpha1;
option go_package = "github.com/holos-run/holos/service/gen/holos/v1alpha1;holos";
import "google/protobuf/timestamp.proto";
// git clone https://github.com/bufbuild/protovalidate then add <parent>/protovalidate/proto/protovalidate to your editor proto search path
import "buf/validate/validate.proto";
// For validation, see the [Standard constraints](https://github.com/bufbuild/protovalidate/blob/main/docs/standard-constraints.md)
message Timestamps {
// Created at timestamp
google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.lt_now = true];
// Updated at timestamp
google.protobuf.Timestamp updated_at = 2 [(buf.validate.field).timestamp.lt_now = true];
}

View File

@@ -7,28 +7,10 @@ option go_package = "github.com/holos-run/holos/service/gen/holos/v1alpha1;holos
import "google/protobuf/timestamp.proto";
// git clone https://github.com/bufbuild/protovalidate then add <parent>/protovalidate/proto/protovalidate to your editor proto search path
import "buf/validate/validate.proto";
import "holos/v1alpha1/timestamps.proto";
// For validation, see the [Standard constraints](https://github.com/bufbuild/protovalidate/blob/main/docs/standard-constraints.md)
message Timestamps {
// Created at timestamp
google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.lt_now = true];
// Updated at timestamp
google.protobuf.Timestamp updated_at = 2 [(buf.validate.field).timestamp.lt_now = true];
}
// Empty request, claims are pulled from the id token
message GetUserClaimsRequest {}
// UserClaims represents id token claims
message GetUserClaimsResponse {
string iss = 1;
string sub = 2;
string email = 3 [(buf.validate.field).string.email = true];
bool email_verified = 4;
string name = 5 [(buf.validate.field).string.max_len = 100];
}
// User represents a human user in the system. See db schema in ent/schema/user.go
message User {
// Unique id assigned by the server.
@@ -39,16 +21,40 @@ message User {
Timestamps timestamps = 5;
}
message RegisterUserRequest {
optional string name = 1 [(buf.validate.field).string.max_len = 100];
}
message CreateCallerUserRequest {}
message RegisterUserResponse {
message CreateCallerUserResponse {
User user = 1;
bool already_exists = 2;
}
service HolosService {
rpc GetUserClaims(GetUserClaimsRequest) returns (GetUserClaimsResponse) {}
rpc RegisterUser(RegisterUserRequest) returns (RegisterUserResponse) {}
message GetCallerClaimsRequest {}
message Claims {
string iss = 1;
string sub = 2;
string email = 3 [(buf.validate.field).string.email = true];
bool email_verified = 4;
string name = 5 [(buf.validate.field).string.max_len = 100];
repeated string groups = 6;
string given_name = 7;
string family_name = 8;
string picture = 9;
}
// UserClaims represents id token claims
message GetCallerClaimsResponse {
Claims claims = 1;
}
// Empty request, claims are pulled from the id token
message GetCallerUserRequest {}
message GetCallerUserResponse {
User user = 1;
}
service UserService {
rpc GetCallerClaims(GetCallerClaimsRequest) returns (GetCallerClaimsResponse) {}
rpc GetCallerUser(GetCallerUserRequest) returns (GetCallerUserResponse) {}
rpc CreateCallerUser(CreateCallerUserRequest) returns (CreateCallerUserResponse) {}
}

View File

@@ -2,6 +2,6 @@ package holos
// These imports are here to keep go mod tidy from constantly creating a dirty git state which makes goreleaser fail.
import (
_ "github.com/olekukonko/tablewriter"
_ "github.com/mattn/go-runewidth"
_ "github.com/mattn/go-runewidth"
_ "github.com/olekukonko/tablewriter"
)

View File

@@ -1 +1 @@
68
70

View File

@@ -1 +1 @@
1
0